diff --git a/library/AndroidManifest.xml b/library/AndroidManifest.xml index 150baa85c0..fdd26b94d8 100644 --- a/library/AndroidManifest.xml +++ b/library/AndroidManifest.xml @@ -3,6 +3,6 @@ package="org.whispersystems.textsecure" android:versionCode="1" android:versionName="0.1"> - + diff --git a/library/build.gradle b/library/build.gradle index cd7eab85fc..1960626d70 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -4,7 +4,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:0.6.1' + classpath 'com.android.tools.build:gradle:0.7.+' } } diff --git a/library/protobuf/IncomingPushMessageSignal.proto b/library/protobuf/IncomingPushMessageSignal.proto index 08faf9671b..b3294a5fbb 100644 --- a/library/protobuf/IncomingPushMessageSignal.proto +++ b/library/protobuf/IncomingPushMessageSignal.proto @@ -6,6 +6,7 @@ option java_outer_classname = "PushMessageProtos"; message IncomingPushMessageSignal { optional uint32 type = 1; optional string source = 2; + optional uint32 sourceDevice = 7; optional string relay = 3; // repeated string destinations = 4; // No longer supported optional uint64 timestamp = 5; diff --git a/library/src/org/whispersystems/textsecure/crypto/SessionCipher.java b/library/src/org/whispersystems/textsecure/crypto/SessionCipher.java index 92c8cde72e..ebbe2ed1d6 100644 --- a/library/src/org/whispersystems/textsecure/crypto/SessionCipher.java +++ b/library/src/org/whispersystems/textsecure/crypto/SessionCipher.java @@ -20,7 +20,7 @@ package org.whispersystems.textsecure.crypto; import android.content.Context; import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage; -import org.whispersystems.textsecure.storage.CanonicalRecipientAddress; +import org.whispersystems.textsecure.storage.RecipientDevice; import org.whispersystems.textsecure.storage.SessionRecordV1; import org.whispersystems.textsecure.storage.SessionRecordV2; @@ -31,13 +31,14 @@ public abstract class SessionCipher { public abstract CiphertextMessage encrypt(byte[] paddedMessage); public abstract byte[] decrypt(byte[] decodedMessage) throws InvalidMessageException; - public static SessionCipher createFor(Context context, MasterSecret masterSecret, - CanonicalRecipientAddress recipient) + public static SessionCipher createFor(Context context, + MasterSecret masterSecret, + RecipientDevice recipient) { if (SessionRecordV2.hasSession(context, masterSecret, recipient)) { return new SessionCipherV2(context, masterSecret, recipient); - } else if (SessionRecordV1.hasSession(context, recipient)) { - return new SessionCipherV1(context, masterSecret, recipient); + } else if (SessionRecordV1.hasSession(context, recipient.getRecipientId())) { + return new SessionCipherV1(context, masterSecret, recipient.getRecipient()); } else { throw new AssertionError("Attempt to initialize cipher for non-existing session."); } diff --git a/library/src/org/whispersystems/textsecure/crypto/SessionCipherV1.java b/library/src/org/whispersystems/textsecure/crypto/SessionCipherV1.java index bebdf45872..f9f7a1e48e 100644 --- a/library/src/org/whispersystems/textsecure/crypto/SessionCipherV1.java +++ b/library/src/org/whispersystems/textsecure/crypto/SessionCipherV1.java @@ -9,7 +9,8 @@ import org.whispersystems.textsecure.crypto.kdf.DerivedSecrets; import org.whispersystems.textsecure.crypto.kdf.NKDF; import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage; import org.whispersystems.textsecure.crypto.protocol.WhisperMessageV1; -import org.whispersystems.textsecure.storage.CanonicalRecipientAddress; +import org.whispersystems.textsecure.storage.CanonicalRecipient; +import org.whispersystems.textsecure.storage.RecipientDevice; import org.whispersystems.textsecure.storage.InvalidKeyIdException; import org.whispersystems.textsecure.storage.LocalKeyRecord; import org.whispersystems.textsecure.storage.RemoteKeyRecord; @@ -29,16 +30,17 @@ import javax.crypto.spec.SecretKeySpec; public class SessionCipherV1 extends SessionCipher { - private final Context context; - private final MasterSecret masterSecret; - private final CanonicalRecipientAddress recipient; + private final Context context; + private final MasterSecret masterSecret; + private final CanonicalRecipient recipient; - public SessionCipherV1(Context context, MasterSecret masterSecret, - CanonicalRecipientAddress recipient) + public SessionCipherV1(Context context, + MasterSecret masterSecret, + CanonicalRecipient recipient) { - this.context = context; - this.masterSecret = masterSecret; - this.recipient = recipient; + this.context = context; + this.masterSecret = masterSecret; + this.recipient = recipient; } public CiphertextMessage encrypt(byte[] paddedMessageBody) { @@ -219,7 +221,7 @@ public class SessionCipherV1 extends SessionCipher { } private KeyRecords getKeyRecords(Context context, MasterSecret masterSecret, - CanonicalRecipientAddress recipient) + CanonicalRecipient recipient) { LocalKeyRecord localKeyRecord = new LocalKeyRecord(context, masterSecret, recipient); RemoteKeyRecord remoteKeyRecord = new RemoteKeyRecord(context, recipient); diff --git a/library/src/org/whispersystems/textsecure/crypto/SessionCipherV2.java b/library/src/org/whispersystems/textsecure/crypto/SessionCipherV2.java index b5fd072681..40b5afbe48 100644 --- a/library/src/org/whispersystems/textsecure/crypto/SessionCipherV2.java +++ b/library/src/org/whispersystems/textsecure/crypto/SessionCipherV2.java @@ -1,7 +1,6 @@ package org.whispersystems.textsecure.crypto; import android.content.Context; -import android.util.Log; import android.util.Pair; import org.whispersystems.textsecure.crypto.ecc.Curve; @@ -13,7 +12,7 @@ import org.whispersystems.textsecure.crypto.protocol.WhisperMessageV2; import org.whispersystems.textsecure.crypto.ratchet.ChainKey; import org.whispersystems.textsecure.crypto.ratchet.MessageKeys; import org.whispersystems.textsecure.crypto.ratchet.RootKey; -import org.whispersystems.textsecure.storage.CanonicalRecipientAddress; +import org.whispersystems.textsecure.storage.RecipientDevice; import org.whispersystems.textsecure.storage.SessionRecordV2; import org.whispersystems.textsecure.util.Conversions; @@ -29,13 +28,13 @@ import javax.crypto.spec.SecretKeySpec; public class SessionCipherV2 extends SessionCipher { - private final Context context; - private final MasterSecret masterSecret; - private final CanonicalRecipientAddress recipient; + private final Context context; + private final MasterSecret masterSecret; + private final RecipientDevice recipient; public SessionCipherV2(Context context, MasterSecret masterSecret, - CanonicalRecipientAddress recipient) + RecipientDevice recipient) { this.context = context; this.masterSecret = masterSecret; diff --git a/library/src/org/whispersystems/textsecure/push/IncomingPushMessage.java b/library/src/org/whispersystems/textsecure/push/IncomingPushMessage.java index 34bf72ff12..33a89b65e6 100644 --- a/library/src/org/whispersystems/textsecure/push/IncomingPushMessage.java +++ b/library/src/org/whispersystems/textsecure/push/IncomingPushMessage.java @@ -37,6 +37,7 @@ public class IncomingPushMessage implements PushMessage, Parcelable { private int type; private String source; + private int sourceDevice; private byte[] message; private long timestamp; private String relay; @@ -44,6 +45,7 @@ public class IncomingPushMessage implements PushMessage, Parcelable { private IncomingPushMessage(IncomingPushMessage message, byte[] body) { this.type = message.type; this.source = message.source; + this.sourceDevice = message.sourceDevice; this.timestamp = message.timestamp; this.relay = message.relay; this.message = body; @@ -52,14 +54,16 @@ public class IncomingPushMessage implements PushMessage, Parcelable { public IncomingPushMessage(IncomingPushMessageSignal signal) { this.type = signal.getType(); this.source = signal.getSource(); + this.sourceDevice = signal.getSourceDevice(); this.message = signal.getMessage().toByteArray(); this.timestamp = signal.getTimestamp(); this.relay = signal.getRelay(); } public IncomingPushMessage(Parcel in) { - this.type = in.readInt(); - this.source = in.readString(); + this.type = in.readInt(); + this.source = in.readString(); + this.sourceDevice = in.readInt(); if (in.readInt() == 1) { this.relay = in.readString(); @@ -70,11 +74,12 @@ public class IncomingPushMessage implements PushMessage, Parcelable { this.timestamp = in.readLong(); } - public IncomingPushMessage(int type, String source, + public IncomingPushMessage(int type, String source, int sourceDevice, byte[] body, long timestamp) { this.type = type; this.source = source; + this.sourceDevice = sourceDevice; this.message = body; this.timestamp = timestamp; } @@ -91,6 +96,10 @@ public class IncomingPushMessage implements PushMessage, Parcelable { return source; } + public int getSourceDevice() { + return sourceDevice; + } + public byte[] getBody() { return message; } @@ -104,6 +113,7 @@ public class IncomingPushMessage implements PushMessage, Parcelable { public void writeToParcel(Parcel dest, int flags) { dest.writeInt(type); dest.writeString(source); + dest.writeInt(sourceDevice); dest.writeInt(relay == null ? 0 : 1); if (relay != null) { dest.writeString(relay); diff --git a/library/src/org/whispersystems/textsecure/push/MismatchedDevices.java b/library/src/org/whispersystems/textsecure/push/MismatchedDevices.java new file mode 100644 index 0000000000..714a76d754 --- /dev/null +++ b/library/src/org/whispersystems/textsecure/push/MismatchedDevices.java @@ -0,0 +1,17 @@ +package org.whispersystems.textsecure.push; + +import java.util.List; + +public class MismatchedDevices { + private List missingDevices; + + private List extraDevices; + + public List getMissingDevices() { + return missingDevices; + } + + public List getExtraDevices() { + return extraDevices; + } +} diff --git a/library/src/org/whispersystems/textsecure/push/MismatchedDevicesException.java b/library/src/org/whispersystems/textsecure/push/MismatchedDevicesException.java new file mode 100644 index 0000000000..9b34d4f5c5 --- /dev/null +++ b/library/src/org/whispersystems/textsecure/push/MismatchedDevicesException.java @@ -0,0 +1,16 @@ +package org.whispersystems.textsecure.push; + +import java.io.IOException; + +public class MismatchedDevicesException extends IOException { + + private final MismatchedDevices mismatchedDevices; + + public MismatchedDevicesException(MismatchedDevices mismatchedDevices) { + this.mismatchedDevices = mismatchedDevices; + } + + public MismatchedDevices getMismatchedDevices() { + return mismatchedDevices; + } +} diff --git a/library/src/org/whispersystems/textsecure/push/OutgoingPushMessage.java b/library/src/org/whispersystems/textsecure/push/OutgoingPushMessage.java index c2ce11d82c..ea797ec346 100644 --- a/library/src/org/whispersystems/textsecure/push/OutgoingPushMessage.java +++ b/library/src/org/whispersystems/textsecure/push/OutgoingPushMessage.java @@ -21,23 +21,17 @@ import org.whispersystems.textsecure.util.Base64; public class OutgoingPushMessage implements PushMessage { private int type; - private String destination; + private int destinationDeviceId; private String body; - private String relay; - public OutgoingPushMessage(String destination, byte[] body, int type) { - this(null, destination, body, type); + public OutgoingPushMessage(PushAddress address, PushBody body) { + this.type = body.getType(); + this.destinationDeviceId = address.getDeviceId(); + this.body = Base64.encodeBytes(body.getBody()); } - public OutgoingPushMessage(String relay, String destination, byte[] body, int type) { - this.relay = relay; - this.destination = destination; - this.body = Base64.encodeBytes(body); - this.type = type; - } - - public String getDestination() { - return destination; + public int getDestinationDeviceId() { + return destinationDeviceId; } public String getBody() { @@ -47,8 +41,4 @@ public class OutgoingPushMessage implements PushMessage { public int getType() { return type; } - - public String getRelay() { - return relay; - } } diff --git a/library/src/org/whispersystems/textsecure/push/OutgoingPushMessageList.java b/library/src/org/whispersystems/textsecure/push/OutgoingPushMessageList.java index 6504ba5b7a..3b8525857b 100644 --- a/library/src/org/whispersystems/textsecure/push/OutgoingPushMessageList.java +++ b/library/src/org/whispersystems/textsecure/push/OutgoingPushMessageList.java @@ -5,18 +5,27 @@ import java.util.List; public class OutgoingPushMessageList { + private String destination; + + private String relay; + private List messages; - public OutgoingPushMessageList(OutgoingPushMessage message) { - this.messages = new LinkedList(); - this.messages.add(message); + public OutgoingPushMessageList(String destination, String relay, List messages) { + this.destination = destination; + this.relay = relay; + this.messages = messages; } - public OutgoingPushMessageList(List messages) { - this.messages = messages; + public String getDestination() { + return destination; } public List getMessages() { return messages; } + + public String getRelay() { + return relay; + } } diff --git a/library/src/org/whispersystems/textsecure/push/PreKeyEntity.java b/library/src/org/whispersystems/textsecure/push/PreKeyEntity.java index 1f9cab90e4..ac967e4e7b 100644 --- a/library/src/org/whispersystems/textsecure/push/PreKeyEntity.java +++ b/library/src/org/whispersystems/textsecure/push/PreKeyEntity.java @@ -8,6 +8,7 @@ import com.google.thoughtcrimegson.JsonParseException; import com.google.thoughtcrimegson.JsonPrimitive; import com.google.thoughtcrimegson.JsonSerializationContext; import com.google.thoughtcrimegson.JsonSerializer; +import com.google.thoughtcrimegson.annotations.Expose; import org.whispersystems.textsecure.crypto.IdentityKey; import org.whispersystems.textsecure.crypto.InvalidKeyException; @@ -20,6 +21,9 @@ import java.lang.reflect.Type; public class PreKeyEntity { + @Expose(serialize = false) + private int deviceId; + private int keyId; private ECPublicKey publicKey; private IdentityKey identityKey; @@ -30,6 +34,10 @@ public class PreKeyEntity { this.identityKey = identityKey; } + public int getDeviceId() { + return deviceId; + } + public int getKeyId() { return keyId; } diff --git a/library/src/org/whispersystems/textsecure/push/PushAddress.java b/library/src/org/whispersystems/textsecure/push/PushAddress.java new file mode 100644 index 0000000000..0522cec920 --- /dev/null +++ b/library/src/org/whispersystems/textsecure/push/PushAddress.java @@ -0,0 +1,32 @@ +package org.whispersystems.textsecure.push; + +import android.content.Context; + +import org.whispersystems.textsecure.directory.Directory; +import org.whispersystems.textsecure.storage.RecipientDevice; + +public class PushAddress extends RecipientDevice { + + private final String e164number; + private final String relay; + + private PushAddress(long recipientId, String e164number, int deviceId, String relay) { + super(recipientId, deviceId); + this.e164number = e164number; + this.relay = relay; + } + + public String getNumber() { + return e164number; + } + + public String getRelay() { + return relay; + } + + public static PushAddress create(Context context, long recipientId, String e164number, int deviceId) { + String relay = Directory.getInstance(context).getRelay(e164number); + return new PushAddress(recipientId, e164number, deviceId, relay); + } + +} diff --git a/library/src/org/whispersystems/textsecure/push/PushDestination.java b/library/src/org/whispersystems/textsecure/push/PushDestination.java deleted file mode 100644 index 1126becc9b..0000000000 --- a/library/src/org/whispersystems/textsecure/push/PushDestination.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.whispersystems.textsecure.push; - -import android.content.Context; - -import org.whispersystems.textsecure.directory.Directory; -import org.whispersystems.textsecure.util.InvalidNumberException; -import org.whispersystems.textsecure.util.PhoneNumberFormatter; - -public class PushDestination { - - private final String e164number; - private final String relay; - - private PushDestination(String e164number, String relay) { - this.e164number = e164number; - this.relay = relay; - } - - public String getNumber() { - return e164number; - } - - public String getRelay() { - return relay; - } - - public static PushDestination create(Context context, - String localNumber, - String destinationNumber) - throws InvalidNumberException - { - String e164destination = PhoneNumberFormatter.formatNumber(destinationNumber, localNumber); - String relay = Directory.getInstance(context).getRelay(e164destination); - - return new PushDestination(e164destination, relay); - } -} diff --git a/library/src/org/whispersystems/textsecure/push/PushMessageProtos.java b/library/src/org/whispersystems/textsecure/push/PushMessageProtos.java index 840ddfd749..78db4402c6 100644 --- a/library/src/org/whispersystems/textsecure/push/PushMessageProtos.java +++ b/library/src/org/whispersystems/textsecure/push/PushMessageProtos.java @@ -19,6 +19,10 @@ public final class PushMessageProtos { boolean hasSource(); String getSource(); + // optional uint32 sourceDevice = 7; + boolean hasSourceDevice(); + int getSourceDevice(); + // optional string relay = 3; boolean hasRelay(); String getRelay(); @@ -102,11 +106,21 @@ public final class PushMessageProtos { } } + // optional uint32 sourceDevice = 7; + public static final int SOURCEDEVICE_FIELD_NUMBER = 7; + private int sourceDevice_; + public boolean hasSourceDevice() { + return ((bitField0_ & 0x00000004) == 0x00000004); + } + public int getSourceDevice() { + return sourceDevice_; + } + // optional string relay = 3; public static final int RELAY_FIELD_NUMBER = 3; private java.lang.Object relay_; public boolean hasRelay() { - return ((bitField0_ & 0x00000004) == 0x00000004); + return ((bitField0_ & 0x00000008) == 0x00000008); } public String getRelay() { java.lang.Object ref = relay_; @@ -138,7 +152,7 @@ public final class PushMessageProtos { public static final int TIMESTAMP_FIELD_NUMBER = 5; private long timestamp_; public boolean hasTimestamp() { - return ((bitField0_ & 0x00000008) == 0x00000008); + return ((bitField0_ & 0x00000010) == 0x00000010); } public long getTimestamp() { return timestamp_; @@ -148,7 +162,7 @@ public final class PushMessageProtos { public static final int MESSAGE_FIELD_NUMBER = 6; private com.google.protobuf.ByteString message_; public boolean hasMessage() { - return ((bitField0_ & 0x00000010) == 0x00000010); + return ((bitField0_ & 0x00000020) == 0x00000020); } public com.google.protobuf.ByteString getMessage() { return message_; @@ -157,6 +171,7 @@ public final class PushMessageProtos { private void initFields() { type_ = 0; source_ = ""; + sourceDevice_ = 0; relay_ = ""; timestamp_ = 0L; message_ = com.google.protobuf.ByteString.EMPTY; @@ -179,15 +194,18 @@ public final class PushMessageProtos { if (((bitField0_ & 0x00000002) == 0x00000002)) { output.writeBytes(2, getSourceBytes()); } - if (((bitField0_ & 0x00000004) == 0x00000004)) { + if (((bitField0_ & 0x00000008) == 0x00000008)) { output.writeBytes(3, getRelayBytes()); } - if (((bitField0_ & 0x00000008) == 0x00000008)) { + if (((bitField0_ & 0x00000010) == 0x00000010)) { output.writeUInt64(5, timestamp_); } - if (((bitField0_ & 0x00000010) == 0x00000010)) { + if (((bitField0_ & 0x00000020) == 0x00000020)) { output.writeBytes(6, message_); } + if (((bitField0_ & 0x00000004) == 0x00000004)) { + output.writeUInt32(7, sourceDevice_); + } getUnknownFields().writeTo(output); } @@ -205,18 +223,22 @@ public final class PushMessageProtos { size += com.google.protobuf.CodedOutputStream .computeBytesSize(2, getSourceBytes()); } - if (((bitField0_ & 0x00000004) == 0x00000004)) { + if (((bitField0_ & 0x00000008) == 0x00000008)) { size += com.google.protobuf.CodedOutputStream .computeBytesSize(3, getRelayBytes()); } - if (((bitField0_ & 0x00000008) == 0x00000008)) { + if (((bitField0_ & 0x00000010) == 0x00000010)) { size += com.google.protobuf.CodedOutputStream .computeUInt64Size(5, timestamp_); } - if (((bitField0_ & 0x00000010) == 0x00000010)) { + if (((bitField0_ & 0x00000020) == 0x00000020)) { size += com.google.protobuf.CodedOutputStream .computeBytesSize(6, message_); } + if (((bitField0_ & 0x00000004) == 0x00000004)) { + size += com.google.protobuf.CodedOutputStream + .computeUInt32Size(7, sourceDevice_); + } size += getUnknownFields().getSerializedSize(); memoizedSerializedSize = size; return size; @@ -345,12 +367,14 @@ public final class PushMessageProtos { bitField0_ = (bitField0_ & ~0x00000001); source_ = ""; bitField0_ = (bitField0_ & ~0x00000002); - relay_ = ""; + sourceDevice_ = 0; bitField0_ = (bitField0_ & ~0x00000004); - timestamp_ = 0L; + relay_ = ""; bitField0_ = (bitField0_ & ~0x00000008); - message_ = com.google.protobuf.ByteString.EMPTY; + timestamp_ = 0L; bitField0_ = (bitField0_ & ~0x00000010); + message_ = com.google.protobuf.ByteString.EMPTY; + bitField0_ = (bitField0_ & ~0x00000020); return this; } @@ -400,14 +424,18 @@ public final class PushMessageProtos { if (((from_bitField0_ & 0x00000004) == 0x00000004)) { to_bitField0_ |= 0x00000004; } - result.relay_ = relay_; + result.sourceDevice_ = sourceDevice_; if (((from_bitField0_ & 0x00000008) == 0x00000008)) { to_bitField0_ |= 0x00000008; } - result.timestamp_ = timestamp_; + result.relay_ = relay_; if (((from_bitField0_ & 0x00000010) == 0x00000010)) { to_bitField0_ |= 0x00000010; } + result.timestamp_ = timestamp_; + if (((from_bitField0_ & 0x00000020) == 0x00000020)) { + to_bitField0_ |= 0x00000020; + } result.message_ = message_; result.bitField0_ = to_bitField0_; onBuilt(); @@ -431,6 +459,9 @@ public final class PushMessageProtos { if (other.hasSource()) { setSource(other.getSource()); } + if (other.hasSourceDevice()) { + setSourceDevice(other.getSourceDevice()); + } if (other.hasRelay()) { setRelay(other.getRelay()); } @@ -482,20 +513,25 @@ public final class PushMessageProtos { break; } case 26: { - bitField0_ |= 0x00000004; + bitField0_ |= 0x00000008; relay_ = input.readBytes(); break; } case 40: { - bitField0_ |= 0x00000008; + bitField0_ |= 0x00000010; timestamp_ = input.readUInt64(); break; } case 50: { - bitField0_ |= 0x00000010; + bitField0_ |= 0x00000020; message_ = input.readBytes(); break; } + case 56: { + bitField0_ |= 0x00000004; + sourceDevice_ = input.readUInt32(); + break; + } } } } @@ -559,10 +595,31 @@ public final class PushMessageProtos { onChanged(); } + // optional uint32 sourceDevice = 7; + private int sourceDevice_ ; + public boolean hasSourceDevice() { + return ((bitField0_ & 0x00000004) == 0x00000004); + } + public int getSourceDevice() { + return sourceDevice_; + } + public Builder setSourceDevice(int value) { + bitField0_ |= 0x00000004; + sourceDevice_ = value; + onChanged(); + return this; + } + public Builder clearSourceDevice() { + bitField0_ = (bitField0_ & ~0x00000004); + sourceDevice_ = 0; + onChanged(); + return this; + } + // optional string relay = 3; private java.lang.Object relay_ = ""; public boolean hasRelay() { - return ((bitField0_ & 0x00000004) == 0x00000004); + return ((bitField0_ & 0x00000008) == 0x00000008); } public String getRelay() { java.lang.Object ref = relay_; @@ -578,19 +635,19 @@ public final class PushMessageProtos { if (value == null) { throw new NullPointerException(); } - bitField0_ |= 0x00000004; + bitField0_ |= 0x00000008; relay_ = value; onChanged(); return this; } public Builder clearRelay() { - bitField0_ = (bitField0_ & ~0x00000004); + bitField0_ = (bitField0_ & ~0x00000008); relay_ = getDefaultInstance().getRelay(); onChanged(); return this; } void setRelay(com.google.protobuf.ByteString value) { - bitField0_ |= 0x00000004; + bitField0_ |= 0x00000008; relay_ = value; onChanged(); } @@ -598,19 +655,19 @@ public final class PushMessageProtos { // optional uint64 timestamp = 5; private long timestamp_ ; public boolean hasTimestamp() { - return ((bitField0_ & 0x00000008) == 0x00000008); + return ((bitField0_ & 0x00000010) == 0x00000010); } public long getTimestamp() { return timestamp_; } public Builder setTimestamp(long value) { - bitField0_ |= 0x00000008; + bitField0_ |= 0x00000010; timestamp_ = value; onChanged(); return this; } public Builder clearTimestamp() { - bitField0_ = (bitField0_ & ~0x00000008); + bitField0_ = (bitField0_ & ~0x00000010); timestamp_ = 0L; onChanged(); return this; @@ -619,7 +676,7 @@ public final class PushMessageProtos { // optional bytes message = 6; private com.google.protobuf.ByteString message_ = com.google.protobuf.ByteString.EMPTY; public boolean hasMessage() { - return ((bitField0_ & 0x00000010) == 0x00000010); + return ((bitField0_ & 0x00000020) == 0x00000020); } public com.google.protobuf.ByteString getMessage() { return message_; @@ -628,13 +685,13 @@ public final class PushMessageProtos { if (value == null) { throw new NullPointerException(); } - bitField0_ |= 0x00000010; + bitField0_ |= 0x00000020; message_ = value; onChanged(); return this; } public Builder clearMessage() { - bitField0_ = (bitField0_ & ~0x00000010); + bitField0_ = (bitField0_ & ~0x00000020); message_ = getDefaultInstance().getMessage(); onChanged(); return this; @@ -2800,23 +2857,24 @@ public final class PushMessageProtos { static { java.lang.String[] descriptorData = { "\n\037IncomingPushMessageSignal.proto\022\ntexts" + - "ecure\"l\n\031IncomingPushMessageSignal\022\014\n\004ty" + - "pe\030\001 \001(\r\022\016\n\006source\030\002 \001(\t\022\r\n\005relay\030\003 \001(\t\022" + - "\021\n\ttimestamp\030\005 \001(\004\022\017\n\007message\030\006 \001(\014\"\363\003\n\022" + - "PushMessageContent\022\014\n\004body\030\001 \001(\t\022E\n\013atta" + - "chments\030\002 \003(\01320.textsecure.PushMessageCo" + - "ntent.AttachmentPointer\022:\n\005group\030\003 \001(\0132+" + - ".textsecure.PushMessageContent.GroupCont" + - "ext\032A\n\021AttachmentPointer\022\n\n\002id\030\001 \001(\006\022\023\n\013" + - "contentType\030\002 \001(\t\022\013\n\003key\030\003 \001(\014\032\210\002\n\014Group", - "Context\022\n\n\002id\030\001 \001(\014\022>\n\004type\030\002 \001(\01620.text" + - "secure.PushMessageContent.GroupContext.T" + - "ype\022\014\n\004name\030\003 \001(\t\022\017\n\007members\030\004 \003(\t\022@\n\006av" + - "atar\030\005 \001(\01320.textsecure.PushMessageConte" + - "nt.AttachmentPointer\"K\n\004Type\022\013\n\007UNKNOWN\020" + - "\000\022\n\n\006CREATE\020\001\022\n\n\006MODIFY\020\002\022\013\n\007DELIVER\020\003\022\007" + - "\n\003ADD\020\004\022\010\n\004QUIT\020\005B7\n\"org.whispersystems." + - "textsecure.pushB\021PushMessageProtos" + "ecure\"\202\001\n\031IncomingPushMessageSignal\022\014\n\004t" + + "ype\030\001 \001(\r\022\016\n\006source\030\002 \001(\t\022\024\n\014sourceDevic" + + "e\030\007 \001(\r\022\r\n\005relay\030\003 \001(\t\022\021\n\ttimestamp\030\005 \001(" + + "\004\022\017\n\007message\030\006 \001(\014\"\363\003\n\022PushMessageConten" + + "t\022\014\n\004body\030\001 \001(\t\022E\n\013attachments\030\002 \003(\01320.t" + + "extsecure.PushMessageContent.AttachmentP" + + "ointer\022:\n\005group\030\003 \001(\0132+.textsecure.PushM" + + "essageContent.GroupContext\032A\n\021Attachment" + + "Pointer\022\n\n\002id\030\001 \001(\006\022\023\n\013contentType\030\002 \001(\t", + "\022\013\n\003key\030\003 \001(\014\032\210\002\n\014GroupContext\022\n\n\002id\030\001 \001" + + "(\014\022>\n\004type\030\002 \001(\01620.textsecure.PushMessag" + + "eContent.GroupContext.Type\022\014\n\004name\030\003 \001(\t" + + "\022\017\n\007members\030\004 \003(\t\022@\n\006avatar\030\005 \001(\01320.text" + + "secure.PushMessageContent.AttachmentPoin" + + "ter\"K\n\004Type\022\013\n\007UNKNOWN\020\000\022\n\n\006CREATE\020\001\022\n\n\006" + + "MODIFY\020\002\022\013\n\007DELIVER\020\003\022\007\n\003ADD\020\004\022\010\n\004QUIT\020\005" + + "B7\n\"org.whispersystems.textsecure.pushB\021" + + "PushMessageProtos" }; com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner = new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() { @@ -2828,7 +2886,7 @@ public final class PushMessageProtos { internal_static_textsecure_IncomingPushMessageSignal_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_textsecure_IncomingPushMessageSignal_descriptor, - new java.lang.String[] { "Type", "Source", "Relay", "Timestamp", "Message", }, + new java.lang.String[] { "Type", "Source", "SourceDevice", "Relay", "Timestamp", "Message", }, org.whispersystems.textsecure.push.PushMessageProtos.IncomingPushMessageSignal.class, org.whispersystems.textsecure.push.PushMessageProtos.IncomingPushMessageSignal.Builder.class); internal_static_textsecure_PushMessageContent_descriptor = diff --git a/library/src/org/whispersystems/textsecure/push/PushServiceSocket.java b/library/src/org/whispersystems/textsecure/push/PushServiceSocket.java index e2961917d8..ae750277ae 100644 --- a/library/src/org/whispersystems/textsecure/push/PushServiceSocket.java +++ b/library/src/org/whispersystems/textsecure/push/PushServiceSocket.java @@ -5,6 +5,7 @@ import android.util.Log; import com.google.thoughtcrimegson.Gson; import com.google.thoughtcrimegson.JsonParseException; +import com.google.thoughtcrimegson.JsonSyntaxException; import org.apache.http.conn.ssl.StrictHostnameVerifier; import org.whispersystems.textsecure.crypto.IdentityKey; @@ -42,13 +43,14 @@ public class PushServiceSocket { private static final String VERIFY_ACCOUNT_PATH = "/v1/accounts/code/%s"; private static final String REGISTER_GCM_PATH = "/v1/accounts/gcm/"; private static final String PREKEY_PATH = "/v1/keys/%s"; + private static final String PREKEY_DEVICE_PATH = "/v1/keys/%s/%s"; private static final String DIRECTORY_TOKENS_PATH = "/v1/directory/tokens"; private static final String DIRECTORY_VERIFY_PATH = "/v1/directory/%s"; - private static final String MESSAGE_PATH = "/v1/messages/"; + private static final String MESSAGE_PATH = "/v1/messages/%s"; private static final String ATTACHMENT_PATH = "/v1/attachments/%s"; - private static final boolean ENFORCE_SSL = true; + private static final boolean ENFORCE_SSL = false; private final Context context; private final String serviceUrl; @@ -86,44 +88,14 @@ public class PushServiceSocket { makeRequest(REGISTER_GCM_PATH, "DELETE", null); } - public void sendMessage(PushDestination recipient, PushBody pushBody) + public void sendMessage(OutgoingPushMessageList bundle) throws IOException { - OutgoingPushMessage message = new OutgoingPushMessage(recipient.getRelay(), - recipient.getNumber(), - pushBody.getBody(), - pushBody.getType()); - - sendMessage(new OutgoingPushMessageList(message)); - } - - public void sendMessage(List recipients, List bodies) - throws IOException - { - List messages = new LinkedList(); - - Iterator recipientsIterator = recipients.iterator(); - Iterator bodiesIterator = bodies.iterator(); - - while (recipientsIterator.hasNext()) { - PushDestination recipient = recipientsIterator.next(); - PushBody body = bodiesIterator.next(); - - messages.add(new OutgoingPushMessage(recipient.getRelay(), recipient.getNumber(), - body.getBody(), body.getType())); + try { + makeRequest(String.format(MESSAGE_PATH, bundle.getDestination()), "PUT", new Gson().toJson(bundle)); + } catch (NotFoundException nfe) { + throw new UnregisteredUserException(nfe); } - - sendMessage(new OutgoingPushMessageList(messages)); - } - - private void sendMessage(OutgoingPushMessageList messages) - throws IOException - { - String responseText = makeRequest(MESSAGE_PATH, "POST", new Gson().toJson(messages)); - PushMessageResponse response = new Gson().fromJson(responseText, PushMessageResponse.class); - - if (response.getFailure().size() != 0) - throw new UnregisteredUserException(response.getFailure()); } public void registerPreKeys(IdentityKey identityKey, @@ -150,20 +122,46 @@ public class PushServiceSocket { PreKeyList.toJson(new PreKeyList(lastResortEntity, entities))); } - public PreKeyEntity getPreKey(PushDestination destination) throws IOException { + public List getPreKeys(PushAddress destination) throws IOException { try { - String path = String.format(PREKEY_PATH, destination.getNumber()); + String deviceId = String.valueOf(destination.getDeviceId()); + + if (deviceId.equals("1")) + deviceId = "*"; + + String path = String.format(PREKEY_DEVICE_PATH, destination.getNumber(), deviceId); if (!Util.isEmpty(destination.getRelay())) { path = path + "?relay=" + destination.getRelay(); } String responseText = makeRequest(path, "GET", null); - Log.w("PushServiceSocket", "Got prekey: " + responseText); - return PreKeyEntity.fromJson(responseText); + PreKeyList response = PreKeyList.fromJson(responseText); + + return response.getKeys(); } catch (JsonParseException e) { - Log.w("PushServiceSocket", e); - throw new IOException("Bad prekey"); + throw new IOException(e); + } + } + + public PreKeyEntity getPreKey(PushAddress destination) throws IOException { + try { + String path = String.format(PREKEY_DEVICE_PATH, destination.getNumber(), + String.valueOf(destination.getDeviceId())); + + if (!Util.isEmpty(destination.getRelay())) { + path = path + "?relay=" + destination.getRelay(); + } + + String responseText = makeRequest(path, "GET", null); + PreKeyList response = PreKeyList.fromJson(responseText); + + if (response.getKeys() == null || response.getKeys().size() < 1) + throw new IOException("Empty prekey list"); + + return response.getKeys().get(0); + } catch (JsonParseException e) { + throw new IOException(e); } } @@ -307,17 +305,25 @@ public class PushServiceSocket { } if (connection.getResponseCode() == 413) { + connection.disconnect(); throw new RateLimitException("Rate limit exceeded: " + connection.getResponseCode()); } if (connection.getResponseCode() == 401 || connection.getResponseCode() == 403) { + connection.disconnect(); throw new AuthorizationFailedException("Authorization failed!"); } if (connection.getResponseCode() == 404) { + connection.disconnect(); throw new NotFoundException("Not found"); } + if (connection.getResponseCode() == 409) { + String response = Util.readFully(connection.getErrorStream()); + throw new MismatchedDevicesException(new Gson().fromJson(response, MismatchedDevices.class)); + } + if (connection.getResponseCode() != 200 && connection.getResponseCode() != 204) { throw new IOException("Bad response: " + connection.getResponseCode() + " " + connection.getResponseMessage()); } diff --git a/library/src/org/whispersystems/textsecure/push/UnregisteredUserException.java b/library/src/org/whispersystems/textsecure/push/UnregisteredUserException.java index 3ad0327251..ae87b39006 100644 --- a/library/src/org/whispersystems/textsecure/push/UnregisteredUserException.java +++ b/library/src/org/whispersystems/textsecure/push/UnregisteredUserException.java @@ -5,15 +5,8 @@ import java.util.List; public class UnregisteredUserException extends IOException { - private final List addresses; - - public UnregisteredUserException(List addresses) { - super(); - this.addresses = addresses; - } - - public List getAddresses() { - return addresses; + public UnregisteredUserException(Exception exception) { + super(exception); } } diff --git a/library/src/org/whispersystems/textsecure/storage/CanonicalRecipient.java b/library/src/org/whispersystems/textsecure/storage/CanonicalRecipient.java new file mode 100644 index 0000000000..389cf3f4bb --- /dev/null +++ b/library/src/org/whispersystems/textsecure/storage/CanonicalRecipient.java @@ -0,0 +1,6 @@ +package org.whispersystems.textsecure.storage; + +public interface CanonicalRecipient { +// public String getNumber(); + public long getRecipientId(); +} diff --git a/library/src/org/whispersystems/textsecure/storage/CanonicalRecipientAddress.java b/library/src/org/whispersystems/textsecure/storage/CanonicalRecipientAddress.java deleted file mode 100644 index 7abb19ba4c..0000000000 --- a/library/src/org/whispersystems/textsecure/storage/CanonicalRecipientAddress.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.whispersystems.textsecure.storage; - -import android.content.Context; - -public interface CanonicalRecipientAddress { - public long getCanonicalAddress(Context context); -} diff --git a/library/src/org/whispersystems/textsecure/storage/LocalKeyRecord.java b/library/src/org/whispersystems/textsecure/storage/LocalKeyRecord.java index 042f26df2d..97810179a7 100644 --- a/library/src/org/whispersystems/textsecure/storage/LocalKeyRecord.java +++ b/library/src/org/whispersystems/textsecure/storage/LocalKeyRecord.java @@ -43,24 +43,24 @@ public class LocalKeyRecord extends Record { private final MasterCipher masterCipher; private final MasterSecret masterSecret; - public LocalKeyRecord(Context context, MasterSecret masterSecret, CanonicalRecipientAddress recipient) { - super(context, SESSIONS_DIRECTORY, getFileNameForRecipient(context, recipient)); + public LocalKeyRecord(Context context, MasterSecret masterSecret, CanonicalRecipient recipient) { + super(context, SESSIONS_DIRECTORY, getFileNameForRecipient(recipient)); this.masterSecret = masterSecret; this.masterCipher = new MasterCipher(masterSecret); loadData(); } - public static boolean hasRecord(Context context, CanonicalRecipientAddress recipient) { - Log.w("LocalKeyRecord", "Checking: " + getFileNameForRecipient(context, recipient)); - return Record.hasRecord(context, SESSIONS_DIRECTORY, getFileNameForRecipient(context, recipient)); + public static boolean hasRecord(Context context, CanonicalRecipient recipient) { + Log.w("LocalKeyRecord", "Checking: " + getFileNameForRecipient(recipient)); + return Record.hasRecord(context, SESSIONS_DIRECTORY, getFileNameForRecipient(recipient)); } - public static void delete(Context context, CanonicalRecipientAddress recipient) { - Record.delete(context, SESSIONS_DIRECTORY, getFileNameForRecipient(context, recipient)); + public static void delete(Context context, CanonicalRecipient recipient) { + Record.delete(context, SESSIONS_DIRECTORY, getFileNameForRecipient(recipient)); } - private static String getFileNameForRecipient(Context context, CanonicalRecipientAddress recipient) { - return recipient.getCanonicalAddress(context) + "-local"; + private static String getFileNameForRecipient(CanonicalRecipient recipient) { + return recipient.getRecipientId() + "-local"; } public void advanceKeyIfNecessary(int keyId) { diff --git a/library/src/org/whispersystems/textsecure/storage/RecipientDevice.java b/library/src/org/whispersystems/textsecure/storage/RecipientDevice.java new file mode 100644 index 0000000000..d97d9d460f --- /dev/null +++ b/library/src/org/whispersystems/textsecure/storage/RecipientDevice.java @@ -0,0 +1,31 @@ +package org.whispersystems.textsecure.storage; + +public class RecipientDevice { + + public static final int DEFAULT_DEVICE_ID = 1; + + private final long recipientId; + private final int deviceId; + + public RecipientDevice(long recipientId, int deviceId) { + this.recipientId = recipientId; + this.deviceId = deviceId; + } + + public long getRecipientId() { + return recipientId; + } + + public int getDeviceId() { + return deviceId; + } + + public CanonicalRecipient getRecipient() { + return new CanonicalRecipient() { + @Override + public long getRecipientId() { + return recipientId; + } + }; + } +} diff --git a/library/src/org/whispersystems/textsecure/storage/Record.java b/library/src/org/whispersystems/textsecure/storage/Record.java index 4b69da80c8..35f941b712 100644 --- a/library/src/org/whispersystems/textsecure/storage/Record.java +++ b/library/src/org/whispersystems/textsecure/storage/Record.java @@ -69,13 +69,19 @@ public abstract class Record { } private static File getAddressFile(Context context, String directory, String address) { + File parent = getParentDirectory(context, directory); + + return new File(parent, address); + } + + protected static File getParentDirectory(Context context, String directory) { File parent = new File(context.getFilesDir(), directory); if (!parent.exists()) { parent.mkdirs(); } - return new File(parent, address); + return parent; } protected byte[] readBlob(FileInputStream in) throws IOException { diff --git a/library/src/org/whispersystems/textsecure/storage/RemoteKeyRecord.java b/library/src/org/whispersystems/textsecure/storage/RemoteKeyRecord.java index e3c5606437..7b4059d1e2 100644 --- a/library/src/org/whispersystems/textsecure/storage/RemoteKeyRecord.java +++ b/library/src/org/whispersystems/textsecure/storage/RemoteKeyRecord.java @@ -43,22 +43,22 @@ public class RemoteKeyRecord extends Record { private PublicKey remoteKeyCurrent; private PublicKey remoteKeyLast; - public RemoteKeyRecord(Context context, CanonicalRecipientAddress recipient) { - super(context, SESSIONS_DIRECTORY, getFileNameForRecipient(context, recipient)); + public RemoteKeyRecord(Context context, CanonicalRecipient recipient) { + super(context, SESSIONS_DIRECTORY, getFileNameForRecipient(recipient)); loadData(); } - public static void delete(Context context, CanonicalRecipientAddress recipient) { - delete(context, SESSIONS_DIRECTORY, getFileNameForRecipient(context, recipient)); + public static void delete(Context context, CanonicalRecipient recipient) { + delete(context, SESSIONS_DIRECTORY, getFileNameForRecipient(recipient)); } - public static boolean hasRecord(Context context, CanonicalRecipientAddress recipient) { - Log.w("LocalKeyRecord", "Checking: " + getFileNameForRecipient(context, recipient)); - return hasRecord(context, SESSIONS_DIRECTORY, getFileNameForRecipient(context, recipient)); + public static boolean hasRecord(Context context, CanonicalRecipient recipient) { + Log.w("LocalKeyRecord", "Checking: " + getFileNameForRecipient(recipient)); + return hasRecord(context, SESSIONS_DIRECTORY, getFileNameForRecipient(recipient)); } - private static String getFileNameForRecipient(Context context, CanonicalRecipientAddress recipient) { - return recipient.getCanonicalAddress(context) + "-remote"; + private static String getFileNameForRecipient(CanonicalRecipient recipient) { + return recipient.getRecipientId() + "-remote"; } public void updateCurrentRemoteKey(PublicKey remoteKey) { diff --git a/library/src/org/whispersystems/textsecure/storage/Session.java b/library/src/org/whispersystems/textsecure/storage/Session.java index b57a68f6db..24396acdff 100644 --- a/library/src/org/whispersystems/textsecure/storage/Session.java +++ b/library/src/org/whispersystems/textsecure/storage/Session.java @@ -14,21 +14,21 @@ import org.whispersystems.textsecure.crypto.MasterSecret; public class Session { - public static void clearV1SessionFor(Context context, CanonicalRecipientAddress recipient) { + public static void clearV1SessionFor(Context context, CanonicalRecipient recipient) { //XXX Obviously we should probably do something more thorough here eventually. LocalKeyRecord.delete(context, recipient); RemoteKeyRecord.delete(context, recipient); SessionRecordV1.delete(context, recipient); } - public static void abortSessionFor(Context context, CanonicalRecipientAddress recipient) { + public static void abortSessionFor(Context context, CanonicalRecipient recipient) { Log.w("Session", "Aborting session, deleting keys..."); clearV1SessionFor(context, recipient); - SessionRecordV2.delete(context, recipient); + SessionRecordV2.deleteAll(context, recipient); } public static boolean hasSession(Context context, MasterSecret masterSecret, - CanonicalRecipientAddress recipient) + CanonicalRecipient recipient) { Log.w("Session", "Checking session..."); return hasV1Session(context, recipient) || hasV2Session(context, masterSecret, recipient); @@ -36,42 +36,40 @@ public class Session { public static boolean hasRemoteIdentityKey(Context context, MasterSecret masterSecret, - CanonicalRecipientAddress recipient) + CanonicalRecipient recipient) { - return (hasV2Session(context, masterSecret, recipient) || - (hasV1Session(context, recipient) && - new SessionRecordV1(context, masterSecret, recipient).getIdentityKey() != null)); + return (hasV2Session(context, masterSecret, recipient) || (hasV1Session(context, recipient) && + new SessionRecordV1(context, masterSecret, recipient).getIdentityKey() != null)); } private static boolean hasV2Session(Context context, MasterSecret masterSecret, - CanonicalRecipientAddress recipient) + CanonicalRecipient recipient) { - return SessionRecordV2.hasSession(context, masterSecret, recipient); + return SessionRecordV2.hasSession(context, masterSecret, recipient.getRecipientId(), + RecipientDevice.DEFAULT_DEVICE_ID); } - private static boolean hasV1Session(Context context, CanonicalRecipientAddress recipient) { + private static boolean hasV1Session(Context context, CanonicalRecipient recipient) { return SessionRecordV1.hasSession(context, recipient) && RemoteKeyRecord.hasRecord(context, recipient) && LocalKeyRecord.hasRecord(context, recipient); } public static IdentityKey getRemoteIdentityKey(Context context, MasterSecret masterSecret, - CanonicalRecipientAddress recipient) + CanonicalRecipient recipient) { - if (SessionRecordV2.hasSession(context, masterSecret, recipient)) { - return new SessionRecordV2(context, masterSecret, recipient).getRemoteIdentityKey(); - } else if (SessionRecordV1.hasSession(context, recipient)) { - return new SessionRecordV1(context, masterSecret, recipient).getIdentityKey(); - } else { - return null; - } + return getRemoteIdentityKey(context, masterSecret, recipient.getRecipientId()); } - public static IdentityKey getRemoteIdentityKey(Context context, MasterSecret masterSecret, + public static IdentityKey getRemoteIdentityKey(Context context, + MasterSecret masterSecret, long recipientId) { - if (SessionRecordV2.hasSession(context, masterSecret, recipientId)) { - return new SessionRecordV2(context, masterSecret, recipientId).getRemoteIdentityKey(); + if (SessionRecordV2.hasSession(context, masterSecret, recipientId, + RecipientDevice.DEFAULT_DEVICE_ID)) + { + return new SessionRecordV2(context, masterSecret, recipientId, + RecipientDevice.DEFAULT_DEVICE_ID).getRemoteIdentityKey(); } else if (SessionRecordV1.hasSession(context, recipientId)) { return new SessionRecordV1(context, masterSecret, recipientId).getIdentityKey(); } else { @@ -80,10 +78,14 @@ public class Session { } public static int getSessionVersion(Context context, MasterSecret masterSecret, - CanonicalRecipientAddress recipient) + CanonicalRecipient recipient) { - if (SessionRecordV2.hasSession(context, masterSecret, recipient)) { - return new SessionRecordV2(context, masterSecret, recipient).getSessionVersion(); + if (SessionRecordV2.hasSession(context, masterSecret, + recipient.getRecipientId(), + RecipientDevice.DEFAULT_DEVICE_ID)) + { + return new SessionRecordV2(context, masterSecret, recipient.getRecipientId(), + RecipientDevice.DEFAULT_DEVICE_ID).getSessionVersion(); } else if (SessionRecordV1.hasSession(context, recipient)) { return new SessionRecordV1(context, masterSecret, recipient).getSessionVersion(); } diff --git a/library/src/org/whispersystems/textsecure/storage/SessionRecordV1.java b/library/src/org/whispersystems/textsecure/storage/SessionRecordV1.java index 49b1dce871..28ac91a5a9 100644 --- a/library/src/org/whispersystems/textsecure/storage/SessionRecordV1.java +++ b/library/src/org/whispersystems/textsecure/storage/SessionRecordV1.java @@ -37,8 +37,8 @@ public class SessionRecordV1 extends Record { private final MasterSecret masterSecret; - public SessionRecordV1(Context context, MasterSecret masterSecret, CanonicalRecipientAddress recipient) { - this(context, masterSecret, getRecipientId(context, recipient)); + public SessionRecordV1(Context context, MasterSecret masterSecret, CanonicalRecipient recipient) { + this(context, masterSecret, recipient.getRecipientId()); } public SessionRecordV1(Context context, MasterSecret masterSecret, long recipientId) { @@ -48,12 +48,12 @@ public class SessionRecordV1 extends Record { loadData(); } - public static void delete(Context context, CanonicalRecipientAddress recipient) { - delete(context, SESSIONS_DIRECTORY, getRecipientId(context, recipient) + ""); + public static void delete(Context context, CanonicalRecipient recipient) { + delete(context, SESSIONS_DIRECTORY, recipient.getRecipientId() + ""); } - public static boolean hasSession(Context context, CanonicalRecipientAddress recipient) { - return hasSession(context, getRecipientId(context, recipient)); + public static boolean hasSession(Context context, CanonicalRecipient recipient) { + return hasSession(context, recipient.getRecipientId()); } public static boolean hasSession(Context context, long recipientId) { @@ -61,10 +61,6 @@ public class SessionRecordV1 extends Record { return hasRecord(context, SESSIONS_DIRECTORY, recipientId+""); } - private static long getRecipientId(Context context, CanonicalRecipientAddress recipient) { - return recipient.getCanonicalAddress(context); - } - public void setSessionKey(SessionKey sessionKeyRecord) { this.sessionKeyRecord = sessionKeyRecord; } diff --git a/library/src/org/whispersystems/textsecure/storage/SessionRecordV2.java b/library/src/org/whispersystems/textsecure/storage/SessionRecordV2.java index 18809b22c1..18fc90da59 100644 --- a/library/src/org/whispersystems/textsecure/storage/SessionRecordV2.java +++ b/library/src/org/whispersystems/textsecure/storage/SessionRecordV2.java @@ -39,12 +39,14 @@ import org.whispersystems.textsecure.storage.StorageProtos.SessionStructure.Chai import org.whispersystems.textsecure.storage.StorageProtos.SessionStructure.PendingKeyExchange; import org.whispersystems.textsecure.storage.StorageProtos.SessionStructure.PendingPreKey; +import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.channels.FileChannel; import java.util.Iterator; +import java.util.LinkedList; import java.util.List; import javax.crypto.spec.SecretKeySpec; @@ -64,33 +66,70 @@ public class SessionRecordV2 extends Record { private StorageProtos.SessionStructure sessionStructure = StorageProtos.SessionStructure.newBuilder().build(); - public SessionRecordV2(Context context, MasterSecret masterSecret, CanonicalRecipientAddress recipient) { - this(context, masterSecret, getRecipientId(context, recipient)); + public SessionRecordV2(Context context, MasterSecret masterSecret, RecipientDevice peer) { + this(context, masterSecret, peer.getRecipientId(), peer.getDeviceId()); } - public SessionRecordV2(Context context, MasterSecret masterSecret, long recipientId) { - super(context, SESSIONS_DIRECTORY_V2, recipientId+""); + public SessionRecordV2(Context context, MasterSecret masterSecret, long recipientId, int deviceId) { + super(context, SESSIONS_DIRECTORY_V2, getRecordName(recipientId, deviceId)); this.masterSecret = masterSecret; loadData(); } - public static void delete(Context context, CanonicalRecipientAddress recipient) { - delete(context, SESSIONS_DIRECTORY_V2, getRecipientId(context, recipient) + ""); + private static String getRecordName(long recipientId, int deviceId) { + return recipientId + (deviceId == RecipientDevice.DEFAULT_DEVICE_ID ? "" : "." + deviceId); + } + + public static List getSessionSubDevices(Context context, CanonicalRecipient recipient) { + List results = new LinkedList(); + File parent = getParentDirectory(context, SESSIONS_DIRECTORY_V2); + String[] children = parent.list(); + + if (children == null) return results; + + for (String child : children) { + try { + String[] parts = child.split("[.]", 2); + long sessionRecipientId = Long.parseLong(parts[0]); + + if (sessionRecipientId == recipient.getRecipientId() && parts.length > 1) { + results.add(Integer.parseInt(parts[1])); + } + } catch (NumberFormatException e) { + Log.w("SessionRecordV2", e); + } + } + + return results; + } + + public static void deleteAll(Context context, CanonicalRecipient recipient) { + List devices = getSessionSubDevices(context, recipient); + + delete(context, SESSIONS_DIRECTORY_V2, getRecordName(recipient.getRecipientId(), + RecipientDevice.DEFAULT_DEVICE_ID)); + + for (int device : devices) { + delete(context, SESSIONS_DIRECTORY_V2, getRecordName(recipient.getRecipientId(), device)); + } + } + + public static void delete(Context context, RecipientDevice recipientDevice) { + delete(context, SESSIONS_DIRECTORY_V2, getRecordName(recipientDevice.getRecipientId(), + recipientDevice.getDeviceId())); } public static boolean hasSession(Context context, MasterSecret masterSecret, - CanonicalRecipientAddress recipient) + RecipientDevice recipient) { - return hasSession(context, masterSecret, getRecipientId(context, recipient)); + return hasSession(context, masterSecret, recipient.getRecipientId(), recipient.getDeviceId()); } - public static boolean hasSession(Context context, MasterSecret masterSecret, long recipientId) { - return hasRecord(context, SESSIONS_DIRECTORY_V2, recipientId+"") && - new SessionRecordV2(context, masterSecret, recipientId).hasSenderChain(); - } - - private static long getRecipientId(Context context, CanonicalRecipientAddress recipient) { - return recipient.getCanonicalAddress(context); + public static boolean hasSession(Context context, MasterSecret masterSecret, + long recipientId, int deviceId) + { + return hasRecord(context, SESSIONS_DIRECTORY_V2, getRecordName(recipientId, deviceId)) && + new SessionRecordV2(context, masterSecret, recipientId, deviceId).hasSenderChain(); } public void clear() { diff --git a/library/src/org/whispersystems/textsecure/util/Hex.java b/library/src/org/whispersystems/textsecure/util/Hex.java index 5db73998e5..a22926c796 100644 --- a/library/src/org/whispersystems/textsecure/util/Hex.java +++ b/library/src/org/whispersystems/textsecure/util/Hex.java @@ -16,6 +16,8 @@ */ package org.whispersystems.textsecure.util; +import java.io.IOException; + /** * Utility for generating hex dumps. */ @@ -43,6 +45,36 @@ public class Hex { return buf.toString(); } + public static String toStringCondensed(byte[] bytes) { + StringBuffer buf = new StringBuffer(); + for (int i=0;i> 1]; + + // two characters form the hex value. + for (int i = 0, j = 0; j < len; i++) { + int f = Character.digit(data[j], 16) << 4; + j++; + f = f | Character.digit(data[j], 16); + j++; + out[i] = (byte) (f & 0xFF); + } + + return out; + } + public static String dump(byte[] bytes) { return dump(bytes, 0, bytes.length); } diff --git a/src/org/thoughtcrime/securesms/ContactSelectionActivity.java b/src/org/thoughtcrime/securesms/ContactSelectionActivity.java index 834b06c1f3..6ba192173d 100644 --- a/src/org/thoughtcrime/securesms/ContactSelectionActivity.java +++ b/src/org/thoughtcrime/securesms/ContactSelectionActivity.java @@ -19,15 +19,10 @@ package org.thoughtcrime.securesms; import android.content.Intent; import android.os.Bundle; import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentPagerAdapter; import android.support.v4.app.FragmentTransaction; import android.support.v4.view.ViewPager; -import org.thoughtcrime.securesms.recipients.Recipients; -import org.thoughtcrime.securesms.util.ActionBarUtil; -import org.thoughtcrime.securesms.util.DynamicTheme; - import com.actionbarsherlock.app.ActionBar; import com.actionbarsherlock.app.ActionBar.Tab; import com.actionbarsherlock.app.ActionBar.TabListener; @@ -35,6 +30,14 @@ import com.actionbarsherlock.view.Menu; import com.actionbarsherlock.view.MenuInflater; import com.actionbarsherlock.view.MenuItem; +import org.thoughtcrime.securesms.util.ActionBarUtil; +import org.thoughtcrime.securesms.util.DynamicTheme; + +import java.util.ArrayList; +import java.util.List; + +import static org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData; + /** * Activity container for selecting a list of contacts. Provides a tab frame for * contact, group, and "recent contact" activity tabs. Used by ComposeMessageActivity @@ -52,8 +55,6 @@ public class ContactSelectionActivity extends PassphraseRequiredSherlockFragment private ContactSelectionGroupsFragment groupsFragment; private ContactSelectionRecentFragment recentFragment; - private Recipients recipients; - @Override protected void onCreate(Bundle icicle) { dynamicTheme.onCreate(this); @@ -97,12 +98,12 @@ public class ContactSelectionActivity extends PassphraseRequiredSherlockFragment } private void handleSelectionFinished() { - recipients = contactsFragment.getSelectedContacts(); - recipients.append(recentFragment.getSelectedContacts()); - recipients.append(groupsFragment.getSelectedContacts(this)); + List contacts = contactsFragment.getSelectedContacts(); + contacts.addAll(recentFragment.getSelectedContacts()); + contacts.addAll(groupsFragment.getSelectedContacts(this)); Intent resultIntent = getIntent(); - resultIntent.putExtra("recipients", this.recipients); + resultIntent.putParcelableArrayListExtra("contacts", new ArrayList(contacts)); setResult(RESULT_OK, resultIntent); diff --git a/src/org/thoughtcrime/securesms/ContactSelectionGroupsFragment.java b/src/org/thoughtcrime/securesms/ContactSelectionGroupsFragment.java index 48b90be1df..9b314299a6 100644 --- a/src/org/thoughtcrime/securesms/ContactSelectionGroupsFragment.java +++ b/src/org/thoughtcrime/securesms/ContactSelectionGroupsFragment.java @@ -84,23 +84,17 @@ public class ContactSelectionGroupsFragment extends SherlockListFragment this.getListView().setFocusable(true); } - public Recipients getSelectedContacts(Context context) { - List recipientList = new LinkedList(); + public List getSelectedContacts(Context context) { + List contacts = new LinkedList(); for (GroupData groupData : selectedGroups.values()) { List contactDataList = ContactAccessor.getInstance() - .getGroupMembership(context, groupData.id); + .getGroupMembership(context, groupData.id); - Log.w("GroupSelectionListActivity", "Got contacts in group: " + contactDataList.size()); - - for (ContactData contactData : contactDataList) { - for (NumberData numberData : contactData.numbers) { - recipientList.add(new Recipient(contactData.name, numberData.number, null, null)); - } - } + contacts.addAll(contactDataList); } - return new Recipients(recipientList); + return contacts; } private void addGroup(GroupData groupData) { diff --git a/src/org/thoughtcrime/securesms/ContactSelectionListFragment.java b/src/org/thoughtcrime/securesms/ContactSelectionListFragment.java index a999df00cb..3054700e99 100644 --- a/src/org/thoughtcrime/securesms/ContactSelectionListFragment.java +++ b/src/org/thoughtcrime/securesms/ContactSelectionListFragment.java @@ -45,6 +45,7 @@ import org.thoughtcrime.securesms.contacts.ContactAccessor.NumberData; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipients; +import java.util.Collection; import java.util.HashMap; import java.util.LinkedList; import java.util.List; @@ -96,16 +97,11 @@ public class ContactSelectionListFragment extends SherlockListFragment } - public Recipients getSelectedContacts() { - List recipientList = new LinkedList(); + public List getSelectedContacts() { + List contacts = new LinkedList(); + contacts.addAll(selectedContacts.values()); - for (ContactData contactData : selectedContacts.values()) { - for (NumberData numberData : contactData.numbers) { - recipientList.add(new Recipient(contactData.name, numberData.number, null, null)); - } - } - - return new Recipients(recipientList); + return contacts; } diff --git a/src/org/thoughtcrime/securesms/ContactSelectionRecentFragment.java b/src/org/thoughtcrime/securesms/ContactSelectionRecentFragment.java index bd4efc0eae..b5a4a8d15a 100644 --- a/src/org/thoughtcrime/securesms/ContactSelectionRecentFragment.java +++ b/src/org/thoughtcrime/securesms/ContactSelectionRecentFragment.java @@ -84,16 +84,11 @@ public class ContactSelectionRecentFragment extends SherlockListFragment this.getLoaderManager().initLoader(0, null, this); } - public Recipients getSelectedContacts() { - List recipientList = new LinkedList(); + public List getSelectedContacts() { + List contacts = new LinkedList(); + contacts.addAll(selectedContacts.values()); - for (ContactData contactData : selectedContacts.values()) { - for (NumberData numberData : contactData.numbers) { - recipientList.add(new Recipient(contactData.name, numberData.number, null, null)); - } - } - - return new Recipients(recipientList); + return contacts; } private void addSingleNumberContact(ContactData contactData) { diff --git a/src/org/thoughtcrime/securesms/ConversationActivity.java b/src/org/thoughtcrime/securesms/ConversationActivity.java index a1dd2adb27..b6c6c52ff4 100644 --- a/src/org/thoughtcrime/securesms/ConversationActivity.java +++ b/src/org/thoughtcrime/securesms/ConversationActivity.java @@ -212,10 +212,10 @@ public class ConversationActivity extends PassphraseRequiredSherlockFragmentActi switch (reqCode) { case PICK_CONTACT: - Recipients recipients = data.getParcelableExtra("recipients"); + List contacts = data.getParcelableArrayListExtra("contacts"); - if (recipients != null) - recipientsPanel.addRecipients(recipients); + if (contacts != null) + recipientsPanel.addContacts(contacts); break; case PICK_IMAGE: @@ -912,7 +912,7 @@ public class ConversationActivity extends PassphraseRequiredSherlockFragmentActi allocatedThreadId = MessageSender.sendMms(ConversationActivity.this, masterSecret, recipients, threadId, attachmentManager.getSlideDeck(), body, distributionType, isEncryptedConversation && !forcePlaintext); - } else if (recipients.isEmailRecipient() || !recipients.isSingleRecipient()) { + } else if (recipients.isEmailRecipient() || !recipients.isSingleRecipient() || recipients.isGroupRecipient()) { allocatedThreadId = MessageSender.sendMms(ConversationActivity.this, masterSecret, recipients, threadId, new SlideDeck(), body, distributionType, isEncryptedConversation && !forcePlaintext); diff --git a/src/org/thoughtcrime/securesms/ConversationItem.java b/src/org/thoughtcrime/securesms/ConversationItem.java index a4e3f95ad8..e065b78aa9 100644 --- a/src/org/thoughtcrime/securesms/ConversationItem.java +++ b/src/org/thoughtcrime/securesms/ConversationItem.java @@ -347,6 +347,7 @@ public class ConversationItem extends LinearLayout { private void handleKeyExchangeClicked() { Intent intent = new Intent(context, ReceiveKeyActivity.class); intent.putExtra("recipient", messageRecord.getIndividualRecipient()); + intent.putExtra("recipient_device_id", messageRecord.getRecipientDeviceId()); intent.putExtra("body", messageRecord.getBody().getBody()); intent.putExtra("thread_id", messageRecord.getThreadId()); intent.putExtra("message_id", messageRecord.getId()); diff --git a/src/org/thoughtcrime/securesms/ReceiveKeyActivity.java b/src/org/thoughtcrime/securesms/ReceiveKeyActivity.java index d795695e08..0d236cf89e 100644 --- a/src/org/thoughtcrime/securesms/ReceiveKeyActivity.java +++ b/src/org/thoughtcrime/securesms/ReceiveKeyActivity.java @@ -38,6 +38,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.sms.SmsTransportDetails; import org.thoughtcrime.securesms.util.MemoryCleaner; +import org.whispersystems.textsecure.crypto.IdentityKey; import org.whispersystems.textsecure.crypto.InvalidKeyException; import org.whispersystems.textsecure.crypto.InvalidMessageException; import org.whispersystems.textsecure.crypto.InvalidVersionException; @@ -45,6 +46,7 @@ import org.whispersystems.textsecure.crypto.MasterSecret; import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage; import org.whispersystems.textsecure.crypto.protocol.PreKeyWhisperMessage; import org.whispersystems.textsecure.storage.InvalidKeyIdException; +import org.whispersystems.textsecure.storage.RecipientDevice; import java.io.IOException; @@ -62,6 +64,7 @@ public class ReceiveKeyActivity extends Activity { private Button cancelButton; private Recipient recipient; + private int recipientDeviceId; private long threadId; private long messageId; @@ -126,12 +129,14 @@ public class ReceiveKeyActivity extends Activity { } private boolean isTrusted(KeyExchangeMessage message, PreKeyWhisperMessage messageBundle) { + RecipientDevice recipientDevice = new RecipientDevice(recipient.getRecipientId(), recipientDeviceId); + if (message != null) { KeyExchangeProcessor processor = KeyExchangeProcessor.createFor(this, masterSecret, - recipient, message); + recipientDevice, message); return processor.isTrusted(message); } else if (messageBundle != null) { - KeyExchangeProcessorV2 processor = new KeyExchangeProcessorV2(this, masterSecret, recipient); + KeyExchangeProcessorV2 processor = new KeyExchangeProcessorV2(this, masterSecret, recipientDevice); return processor.isTrusted(messageBundle); } @@ -162,6 +167,7 @@ public class ReceiveKeyActivity extends Activity { this.confirmButton = (Button) findViewById(R.id.ok_button); this.cancelButton = (Button) findViewById(R.id.cancel_button); this.recipient = getIntent().getParcelableExtra("recipient"); + this.recipientDeviceId = getIntent().getIntExtra("recipient_device_id", -1); this.threadId = getIntent().getLongExtra("thread_id", -1); this.messageId = getIntent().getLongExtra("message_id", -1); this.masterSecret = getIntent().getParcelableExtra("master_secret"); @@ -190,7 +196,8 @@ public class ReceiveKeyActivity extends Activity { protected Void doInBackground(Void... params) { if (keyExchangeMessage != null) { try { - KeyExchangeProcessor processor = KeyExchangeProcessor.createFor(ReceiveKeyActivity.this, masterSecret, recipient, keyExchangeMessage); + RecipientDevice recipientDevice = new RecipientDevice(recipient.getRecipientId(), recipientDeviceId); + KeyExchangeProcessor processor = KeyExchangeProcessor.createFor(ReceiveKeyActivity.this, masterSecret, recipientDevice, keyExchangeMessage); processor.processKeyExchangeMessage(keyExchangeMessage, threadId); DatabaseFactory.getEncryptingSmsDatabase(ReceiveKeyActivity.this) .markAsProcessedKeyExchange(messageId); @@ -201,8 +208,9 @@ public class ReceiveKeyActivity extends Activity { } } else if (keyExchangeMessageBundle != null) { try { + RecipientDevice recipientDevice = new RecipientDevice(recipient.getRecipientId(), recipientDeviceId); KeyExchangeProcessorV2 processor = new KeyExchangeProcessorV2(ReceiveKeyActivity.this, - masterSecret, recipient); + masterSecret, recipientDevice); processor.processKeyExchangeMessage(keyExchangeMessageBundle); CiphertextMessage bundledMessage = keyExchangeMessageBundle.getWhisperMessage(); @@ -213,8 +221,8 @@ public class ReceiveKeyActivity extends Activity { .updateBundleMessageBody(masterSecret, messageId, messageBody); DecryptingQueue.scheduleDecryption(ReceiveKeyActivity.this, masterSecret, messageId, - threadId, recipient.getNumber(), messageBody, - true, false); + threadId, recipient.getNumber(), recipientDeviceId, + messageBody, true, false); } catch (InvalidKeyIdException e) { Log.w("ReceiveKeyActivity", e); DatabaseFactory.getEncryptingSmsDatabase(ReceiveKeyActivity.this) diff --git a/src/org/thoughtcrime/securesms/components/RecipientsPanel.java b/src/org/thoughtcrime/securesms/components/RecipientsPanel.java index 8e7770be6b..14aefa4b96 100644 --- a/src/org/thoughtcrime/securesms/components/RecipientsPanel.java +++ b/src/org/thoughtcrime/securesms/components/RecipientsPanel.java @@ -24,6 +24,7 @@ import android.view.View; import android.widget.RelativeLayout; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.contacts.ContactAccessor; import org.thoughtcrime.securesms.contacts.RecipientsAdapter; import org.thoughtcrime.securesms.contacts.RecipientsEditor; import org.thoughtcrime.securesms.recipients.Recipient; @@ -69,6 +70,14 @@ public class RecipientsPanel extends RelativeLayout { else recipientsText.append(number + ", "); } + public void addContacts(List contacts) { + for (ContactAccessor.ContactData contact : contacts) { + for (ContactAccessor.NumberData number : contact.numbers) { + addRecipient(contact.name, number.number); + } + } + } + public void addRecipients(Recipients recipients) { List recipientList = recipients.getRecipientsList(); Iterator iterator = recipientList.iterator(); diff --git a/src/org/thoughtcrime/securesms/crypto/DecryptingQueue.java b/src/org/thoughtcrime/securesms/crypto/DecryptingQueue.java index 0595a6454b..b76e8ca31d 100644 --- a/src/org/thoughtcrime/securesms/crypto/DecryptingQueue.java +++ b/src/org/thoughtcrime/securesms/crypto/DecryptingQueue.java @@ -46,7 +46,9 @@ import org.whispersystems.textsecure.crypto.InvalidVersionException; import org.whispersystems.textsecure.crypto.MasterSecret; import org.whispersystems.textsecure.crypto.SessionCipher; import org.whispersystems.textsecure.push.IncomingPushMessage; +import org.whispersystems.textsecure.storage.RecipientDevice; import org.whispersystems.textsecure.storage.Session; +import org.whispersystems.textsecure.storage.SessionRecordV2; import org.whispersystems.textsecure.util.Hex; import org.whispersystems.textsecure.util.Util; @@ -78,11 +80,12 @@ public class DecryptingQueue { } public static void scheduleDecryption(Context context, MasterSecret masterSecret, - long messageId, long threadId, String originator, + long messageId, long threadId, String originator, int deviceId, String body, boolean isSecureMessage, boolean isKeyExchange) { DecryptionWorkItem runnable = new DecryptionWorkItem(context, masterSecret, messageId, threadId, - originator, body, isSecureMessage, isKeyExchange); + originator, deviceId, body, + isSecureMessage, isKeyExchange); executor.execute(runnable); } @@ -161,11 +164,13 @@ public class DecryptingQueue { long threadId = record.getThreadId(); String body = record.getBody().getBody(); String originator = record.getIndividualRecipient().getNumber(); + int originatorDeviceId = record.getRecipientDeviceId(); boolean isSecureMessage = record.isSecure(); boolean isKeyExchange = record.isKeyExchange(); scheduleDecryption(context, masterSecret, messageId, threadId, - originator, body, isSecureMessage, isKeyExchange); + originator, originatorDeviceId, body, + isSecureMessage, isKeyExchange); } private static class PushDecryptionWorkItem implements Runnable { @@ -186,15 +191,16 @@ public class DecryptingQueue { public void run() { try { - Recipients recipients = RecipientFactory.getRecipientsFromString(context, message.getSource(), false); - Recipient recipient = recipients.getPrimaryRecipient(); + Recipients recipients = RecipientFactory.getRecipientsFromString(context, message.getSource(), false); + Recipient recipient = recipients.getPrimaryRecipient(); + RecipientDevice recipientDevice = new RecipientDevice(recipient.getRecipientId(), message.getSourceDevice()); - if (!Session.hasSession(context, masterSecret, recipient)) { + if (!SessionRecordV2.hasSession(context, masterSecret, recipientDevice)) { sendResult(PushReceiver.RESULT_NO_SESSION); return; } - SessionCipher sessionCipher = SessionCipher.createFor(context, masterSecret, recipient); + SessionCipher sessionCipher = SessionCipher.createFor(context, masterSecret, recipientDevice); byte[] plaintextBody = sessionCipher.decrypt(message.getBody()); message = message.withBody(plaintextBody); @@ -251,10 +257,11 @@ public class DecryptingQueue { MmsDatabase database = DatabaseFactory.getMmsDatabase(context); try { - String messageFrom = pdu.getFrom().getString(); - Recipients recipients = RecipientFactory.getRecipientsFromString(context, messageFrom, false); - Recipient recipient = recipients.getPrimaryRecipient(); - byte[] ciphertextPduBytes = getEncryptedData(); + String messageFrom = pdu.getFrom().getString(); + Recipients recipients = RecipientFactory.getRecipientsFromString(context, messageFrom, false); + Recipient recipient = recipients.getPrimaryRecipient(); + RecipientDevice recipientDevice = new RecipientDevice(recipient.getRecipientId(), RecipientDevice.DEFAULT_DEVICE_ID); + byte[] ciphertextPduBytes = getEncryptedData(); if (ciphertextPduBytes == null) { Log.w("DecryptingQueue", "No encoded PNG data found on parts."); @@ -272,7 +279,7 @@ public class DecryptingQueue { Log.w("DecryptingQueue", "Decrypting: " + Hex.toString(ciphertextPduBytes)); TextTransport transportDetails = new TextTransport(); - SessionCipher sessionCipher = SessionCipher.createFor(context, masterSecret, recipient); + SessionCipher sessionCipher = SessionCipher.createFor(context, masterSecret, recipientDevice); byte[] decodedCiphertext = transportDetails.getDecodedMessage(ciphertextPduBytes); try { @@ -322,11 +329,13 @@ public class DecryptingQueue { private final MasterSecret masterSecret; private final String body; private final String originator; + private final int deviceId; private final boolean isSecureMessage; private final boolean isKeyExchange; public DecryptionWorkItem(Context context, MasterSecret masterSecret, long messageId, long threadId, - String originator, String body, boolean isSecureMessage, boolean isKeyExchange) + String originator, int deviceId, String body, boolean isSecureMessage, + boolean isKeyExchange) { this.context = context; this.messageId = messageId; @@ -334,6 +343,7 @@ public class DecryptingQueue { this.masterSecret = masterSecret; this.body = body; this.originator = originator; + this.deviceId = deviceId; this.isSecureMessage = isSecureMessage; this.isKeyExchange = isKeyExchange; } @@ -343,8 +353,9 @@ public class DecryptingQueue { String plaintextBody; try { - Recipients recipients = RecipientFactory.getRecipientsFromString(context, originator, false); - Recipient recipient = recipients.getPrimaryRecipient(); + Recipients recipients = RecipientFactory.getRecipientsFromString(context, originator, false); + Recipient recipient = recipients.getPrimaryRecipient(); + RecipientDevice recipientDevice = new RecipientDevice(recipient.getRecipientId(), deviceId); if (!Session.hasSession(context, masterSecret, recipient)) { database.markAsNoSession(messageId); @@ -352,7 +363,7 @@ public class DecryptingQueue { } SmsTransportDetails transportDetails = new SmsTransportDetails(); - SessionCipher sessionCipher = SessionCipher.createFor(context, masterSecret, recipient); + SessionCipher sessionCipher = SessionCipher.createFor(context, masterSecret, recipientDevice); byte[] decodedCiphertext = transportDetails.getDecodedMessage(body.getBytes()); byte[] paddedPlaintext = sessionCipher.decrypt(decodedCiphertext); @@ -401,9 +412,10 @@ public class DecryptingQueue { private void handleKeyExchangeProcessing(String plaintxtBody) { if (TextSecurePreferences.isAutoRespondKeyExchangeEnabled(context)) { try { - Recipient recipient = new Recipient(null, originator, null, null); - KeyExchangeMessage message = KeyExchangeMessage.createFor(plaintxtBody); - KeyExchangeProcessor processor = KeyExchangeProcessor.createFor(context, masterSecret, recipient, message); + Recipient recipient = RecipientFactory.getRecipientsFromString(context, originator, false).getPrimaryRecipient(); + RecipientDevice recipientDevice = new RecipientDevice(recipient.getRecipientId(), deviceId); + KeyExchangeMessage message = KeyExchangeMessage.createFor(plaintxtBody); + KeyExchangeProcessor processor = KeyExchangeProcessor.createFor(context, masterSecret, recipientDevice, message); if (processor.isStale(message)) { DatabaseFactory.getEncryptingSmsDatabase(context).markAsStaleKeyExchange(messageId); @@ -420,6 +432,9 @@ public class DecryptingQueue { } catch (InvalidMessageException e) { Log.w("DecryptingQueue", e); DatabaseFactory.getEncryptingSmsDatabase(context).markAsCorruptKeyExchange(messageId); + } catch (RecipientFormattingException e) { + Log.w("DecryptingQueue", e); + DatabaseFactory.getEncryptingSmsDatabase(context).markAsCorruptKeyExchange(messageId); } } } diff --git a/src/org/thoughtcrime/securesms/crypto/KeyExchangeInitiator.java b/src/org/thoughtcrime/securesms/crypto/KeyExchangeInitiator.java index e0d06bba1a..616221bf15 100644 --- a/src/org/thoughtcrime/securesms/crypto/KeyExchangeInitiator.java +++ b/src/org/thoughtcrime/securesms/crypto/KeyExchangeInitiator.java @@ -31,6 +31,7 @@ import org.whispersystems.textsecure.crypto.MasterSecret; import org.whispersystems.textsecure.crypto.ecc.Curve; import org.whispersystems.textsecure.crypto.ecc.ECKeyPair; import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage; +import org.whispersystems.textsecure.storage.RecipientDevice; import org.whispersystems.textsecure.storage.SessionRecordV2; import java.security.NoSuchAlgorithmException; @@ -70,8 +71,9 @@ public class KeyExchangeInitiator { identityKey.getPublicKey()); OutgoingKeyExchangeMessage textMessage = new OutgoingKeyExchangeMessage(recipient, message.serialize()); + RecipientDevice recipientDevice = new RecipientDevice(recipient.getRecipientId(), RecipientDevice.DEFAULT_DEVICE_ID); - SessionRecordV2 sessionRecordV2 = new SessionRecordV2(context, masterSecret, recipient); + SessionRecordV2 sessionRecordV2 = new SessionRecordV2(context, masterSecret, recipientDevice); sessionRecordV2.setPendingKeyExchange(sequence, baseKey, ephemeralKey, identityKey); sessionRecordV2.save(); @@ -81,8 +83,9 @@ public class KeyExchangeInitiator { private static boolean hasInitiatedSession(Context context, MasterSecret masterSecret, Recipient recipient) { + RecipientDevice recipientDevice = new RecipientDevice(recipient.getRecipientId(), RecipientDevice.DEFAULT_DEVICE_ID); return - new SessionRecordV2(context, masterSecret, recipient) + new SessionRecordV2(context, masterSecret, recipientDevice) .hasPendingKeyExchange(); } diff --git a/src/org/thoughtcrime/securesms/crypto/KeyExchangeProcessor.java b/src/org/thoughtcrime/securesms/crypto/KeyExchangeProcessor.java index 998ab1ce99..eef17d4259 100644 --- a/src/org/thoughtcrime/securesms/crypto/KeyExchangeProcessor.java +++ b/src/org/thoughtcrime/securesms/crypto/KeyExchangeProcessor.java @@ -21,8 +21,10 @@ import android.content.Context; import org.thoughtcrime.securesms.crypto.protocol.KeyExchangeMessage; import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.recipients.RecipientFactory; import org.whispersystems.textsecure.crypto.InvalidMessageException; import org.whispersystems.textsecure.crypto.MasterSecret; +import org.whispersystems.textsecure.storage.RecipientDevice; public abstract class KeyExchangeProcessor { @@ -34,9 +36,13 @@ public abstract class KeyExchangeProcessor { throws InvalidMessageException; public static KeyExchangeProcessor createFor(Context context, MasterSecret masterSecret, - Recipient recipient, KeyExchangeMessage message) + RecipientDevice recipientDevice, + KeyExchangeMessage message) { - if (message.isLegacy()) return new KeyExchangeProcessorV1(context, masterSecret, recipient); - else return new KeyExchangeProcessorV2(context, masterSecret, recipient); + if (message.isLegacy()) { + return new KeyExchangeProcessorV1(context, masterSecret, recipientDevice.getRecipient()); + } else { + return new KeyExchangeProcessorV2(context, masterSecret, recipientDevice); + } } } \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/crypto/KeyExchangeProcessorV1.java b/src/org/thoughtcrime/securesms/crypto/KeyExchangeProcessorV1.java index d31bdc397d..6c23b23532 100644 --- a/src/org/thoughtcrime/securesms/crypto/KeyExchangeProcessorV1.java +++ b/src/org/thoughtcrime/securesms/crypto/KeyExchangeProcessorV1.java @@ -8,6 +8,7 @@ import org.thoughtcrime.securesms.crypto.protocol.KeyExchangeMessage; import org.thoughtcrime.securesms.crypto.protocol.KeyExchangeMessageV1; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.recipients.RecipientFactory; import org.thoughtcrime.securesms.service.KeyCachingService; import org.thoughtcrime.securesms.sms.MessageSender; import org.thoughtcrime.securesms.sms.OutgoingKeyExchangeMessage; @@ -16,6 +17,7 @@ import org.whispersystems.textsecure.crypto.KeyPair; import org.whispersystems.textsecure.crypto.MasterSecret; import org.whispersystems.textsecure.crypto.ecc.Curve; import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage; +import org.whispersystems.textsecure.storage.CanonicalRecipient; import org.whispersystems.textsecure.storage.LocalKeyRecord; import org.whispersystems.textsecure.storage.RemoteKeyRecord; import org.whispersystems.textsecure.storage.SessionRecordV1; @@ -32,17 +34,17 @@ import java.security.SecureRandom; public class KeyExchangeProcessorV1 extends KeyExchangeProcessor { - private Context context; - private Recipient recipient; - private MasterSecret masterSecret; - private LocalKeyRecord localKeyRecord; - private RemoteKeyRecord remoteKeyRecord; - private SessionRecordV1 sessionRecord; + private Context context; + private CanonicalRecipient recipient; + private MasterSecret masterSecret; + private LocalKeyRecord localKeyRecord; + private RemoteKeyRecord remoteKeyRecord; + private SessionRecordV1 sessionRecord; - public KeyExchangeProcessorV1(Context context, MasterSecret masterSecret, Recipient recipient) { - this.context = context; - this.recipient = recipient; - this.masterSecret = masterSecret; + public KeyExchangeProcessorV1(Context context, MasterSecret masterSecret, CanonicalRecipient recipient) { + this.context = context; + this.recipient = recipient; + this.masterSecret = masterSecret; this.remoteKeyRecord = new RemoteKeyRecord(context, recipient); this.localKeyRecord = new LocalKeyRecord(context, masterSecret, recipient); @@ -55,7 +57,8 @@ public class KeyExchangeProcessorV1 extends KeyExchangeProcessor { } public boolean isTrusted(IdentityKey identityKey) { - return DatabaseFactory.getIdentityDatabase(context).isValidIdentity(masterSecret, recipient, + return DatabaseFactory.getIdentityDatabase(context).isValidIdentity(masterSecret, + recipient.getRecipientId(), identityKey); } @@ -80,8 +83,13 @@ public class KeyExchangeProcessorV1 extends KeyExchangeProcessor { @Override public void processKeyExchangeMessage(KeyExchangeMessage _message, long threadId) { - KeyExchangeMessageV1 message = (KeyExchangeMessageV1)_message; - int initiateKeyId = Conversions.lowBitsToMedium(message.getRemoteKey().getId()); + KeyExchangeMessageV1 message = (KeyExchangeMessageV1) _message; + int initiateKeyId = Conversions.lowBitsToMedium(message.getRemoteKey().getId()); + + Recipient recipient = RecipientFactory.getRecipientsForIds(context, + this.recipient.getRecipientId()+"", + true).getPrimaryRecipient(); + message.getRemoteKey().setId(initiateKeyId); if (needsResponseFromUs()) { @@ -113,7 +121,7 @@ public class KeyExchangeProcessorV1 extends KeyExchangeProcessor { if (message.hasIdentityKey()) { DatabaseFactory.getIdentityDatabase(context) - .saveIdentity(masterSecret, recipient, message.getIdentityKey()); + .saveIdentity(masterSecret, recipient.getRecipientId(), message.getIdentityKey()); } DecryptingQueue.scheduleRogueMessages(context, masterSecret, recipient); @@ -130,7 +138,7 @@ public class KeyExchangeProcessorV1 extends KeyExchangeProcessor { public LocalKeyRecord initializeRecordFor(Context context, MasterSecret masterSecret, - Recipient recipient) + CanonicalRecipient recipient) { Log.w("KeyExchangeProcessorV1", "Initializing local key pairs..."); try { diff --git a/src/org/thoughtcrime/securesms/crypto/KeyExchangeProcessorV2.java b/src/org/thoughtcrime/securesms/crypto/KeyExchangeProcessorV2.java index b0190e4df8..5170e1226d 100644 --- a/src/org/thoughtcrime/securesms/crypto/KeyExchangeProcessorV2.java +++ b/src/org/thoughtcrime/securesms/crypto/KeyExchangeProcessorV2.java @@ -8,6 +8,7 @@ import org.thoughtcrime.securesms.crypto.protocol.KeyExchangeMessage; import org.thoughtcrime.securesms.crypto.protocol.KeyExchangeMessageV2; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.recipients.RecipientFactory; import org.thoughtcrime.securesms.service.KeyCachingService; import org.thoughtcrime.securesms.sms.MessageSender; import org.thoughtcrime.securesms.sms.OutgoingKeyExchangeMessage; @@ -24,6 +25,7 @@ import org.whispersystems.textsecure.crypto.ratchet.RatchetingSession; import org.whispersystems.textsecure.push.PreKeyEntity; import org.whispersystems.textsecure.storage.InvalidKeyIdException; import org.whispersystems.textsecure.storage.PreKeyRecord; +import org.whispersystems.textsecure.storage.RecipientDevice; import org.whispersystems.textsecure.storage.Session; import org.whispersystems.textsecure.storage.SessionRecordV2; import org.whispersystems.textsecure.util.Medium; @@ -37,15 +39,16 @@ import org.whispersystems.textsecure.util.Medium; public class KeyExchangeProcessorV2 extends KeyExchangeProcessor { private Context context; - private Recipient recipient; + private RecipientDevice recipientDevice; private MasterSecret masterSecret; private SessionRecordV2 sessionRecord; - public KeyExchangeProcessorV2(Context context, MasterSecret masterSecret, Recipient recipient) { + public KeyExchangeProcessorV2(Context context, MasterSecret masterSecret, RecipientDevice recipientDevice) + { this.context = context; - this.recipient = recipient; + this.recipientDevice = recipientDevice; this.masterSecret = masterSecret; - this.sessionRecord = new SessionRecordV2(context, masterSecret, recipient); + this.sessionRecord = new SessionRecordV2(context, masterSecret, recipientDevice); } public boolean isTrusted(PreKeyWhisperMessage message) { @@ -57,7 +60,8 @@ public class KeyExchangeProcessorV2 extends KeyExchangeProcessor { } public boolean isTrusted(IdentityKey identityKey) { - return DatabaseFactory.getIdentityDatabase(context).isValidIdentity(masterSecret, recipient, + return DatabaseFactory.getIdentityDatabase(context).isValidIdentity(masterSecret, + recipientDevice.getRecipientId(), identityKey); } @@ -80,7 +84,7 @@ public class KeyExchangeProcessorV2 extends KeyExchangeProcessor { Log.w("KeyExchangeProcessor", "Received pre-key with local key ID: " + preKeyId); - if (!PreKeyRecord.hasRecord(context, preKeyId) && Session.hasSession(context, masterSecret, recipient)) { + if (!PreKeyRecord.hasRecord(context, preKeyId) && SessionRecordV2.hasSession(context, masterSecret, recipientDevice)) { Log.w("KeyExchangeProcessor", "We've already processed the prekey part, letting bundled message fall through..."); return; } @@ -97,7 +101,7 @@ public class KeyExchangeProcessorV2 extends KeyExchangeProcessor { RatchetingSession.initializeSession(sessionRecord, ourBaseKey, theirBaseKey, ourEphemeralKey, theirEphemeralKey, ourIdentityKey, theirIdentityKey); - + Session.clearV1SessionFor(context, recipientDevice.getRecipient()); sessionRecord.save(); if (preKeyId != Medium.MAX_VALUE) { @@ -105,7 +109,7 @@ public class KeyExchangeProcessorV2 extends KeyExchangeProcessor { } DatabaseFactory.getIdentityDatabase(context) - .saveIdentity(masterSecret, recipient, theirIdentityKey); + .saveIdentity(masterSecret, recipientDevice.getRecipientId(), theirIdentityKey); } public void processKeyExchangeMessage(PreKeyEntity message, long threadId) @@ -129,7 +133,7 @@ public class KeyExchangeProcessorV2 extends KeyExchangeProcessor { sessionRecord.save(); DatabaseFactory.getIdentityDatabase(context) - .saveIdentity(masterSecret, recipient, message.getIdentityKey()); + .saveIdentity(masterSecret, recipientDevice.getRecipientId(), message.getIdentityKey()); broadcastSecurityUpdateEvent(context, threadId); } @@ -140,6 +144,10 @@ public class KeyExchangeProcessorV2 extends KeyExchangeProcessor { { try { KeyExchangeMessageV2 message = (KeyExchangeMessageV2)_message; + Recipient recipient = RecipientFactory.getRecipientsForIds(context, + String.valueOf(recipientDevice.getRecipientId()), + false) + .getPrimaryRecipient(); Log.w("KeyExchangeProcessorV2", "Received key exchange with sequence: " + message.getSequence()); @@ -197,11 +205,11 @@ public class KeyExchangeProcessorV2 extends KeyExchangeProcessor { ourIdentityKey, message.getIdentityKey()); sessionRecord.setSessionVersion(message.getVersion()); - Session.clearV1SessionFor(context, recipient); + Session.clearV1SessionFor(context, recipientDevice.getRecipient()); sessionRecord.save(); DatabaseFactory.getIdentityDatabase(context) - .saveIdentity(masterSecret, recipient, message.getIdentityKey()); + .saveIdentity(masterSecret, recipientDevice.getRecipientId(), message.getIdentityKey()); DecryptingQueue.scheduleRogueMessages(context, masterSecret, recipient); diff --git a/src/org/thoughtcrime/securesms/database/DatabaseFactory.java b/src/org/thoughtcrime/securesms/database/DatabaseFactory.java index 3bc6b49819..7de9b696b6 100644 --- a/src/org/thoughtcrime/securesms/database/DatabaseFactory.java +++ b/src/org/thoughtcrime/securesms/database/DatabaseFactory.java @@ -644,6 +644,9 @@ public class DatabaseFactory { if (oldVersion < INTRODUCED_GROUP_DATABASE_VERSION) { db.execSQL("CREATE TABLE groups (_id INTEGER PRIMARY KEY, group_id TEXT, owner TEXT, title TEXT, members TEXT, avatar BLOB, avatar_id INTEGER, avatar_key BLOB, avatar_content_type TEXT, timestamp INTEGER);"); db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS group_id_index ON groups (GROUP_ID);"); + db.execSQL("ALTER TABLE push ADD COLUMN device_id INTEGER DEFAULT 1;"); + db.execSQL("ALTER TABLE sms ADD COLUMN address_device_id INTEGER DEFAULT 1;"); + db.execSQL("ALTER TABLE mms ADD COLUMN address_device_id INTEGER DEFAULT 1;"); } db.setTransactionSuccessful(); diff --git a/src/org/thoughtcrime/securesms/database/GroupDatabase.java b/src/org/thoughtcrime/securesms/database/GroupDatabase.java index 1ccb077e5f..ef06e04d81 100644 --- a/src/org/thoughtcrime/securesms/database/GroupDatabase.java +++ b/src/org/thoughtcrime/securesms/database/GroupDatabase.java @@ -6,11 +6,18 @@ import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteOpenHelper; import android.graphics.Bitmap; +import android.util.Log; +import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.recipients.RecipientFactory; +import org.thoughtcrime.securesms.recipients.RecipientFormattingException; +import org.thoughtcrime.securesms.recipients.Recipients; import org.thoughtcrime.securesms.util.BitmapUtil; +import org.thoughtcrime.securesms.util.GroupUtil; import org.whispersystems.textsecure.util.Hex; import org.whispersystems.textsecure.util.Util; +import java.io.IOException; import java.util.LinkedList; import java.util.List; @@ -28,7 +35,7 @@ public class GroupDatabase extends Database { private static final String AVATAR_ID = "avatar_id"; private static final String AVATAR_KEY = "avatar_key"; private static final String AVATAR_CONTENT_TYPE = "avatar_content_type"; - private static final String RELAY = "relay"; + private static final String AVATAR_RELAY = "avatar_relay"; private static final String TIMESTAMP = "timestamp"; public static final String CREATE_TABLE = @@ -42,6 +49,7 @@ public class GroupDatabase extends Database { AVATAR_ID + " INTEGER, " + AVATAR_KEY + " BLOB, " + AVATAR_CONTENT_TYPE + " TEXT, " + + AVATAR_RELAY + " TEXT, " + TIMESTAMP + " INTEGER);"; public static final String[] CREATE_INDEXS = { @@ -52,19 +60,36 @@ public class GroupDatabase extends Database { super(context, databaseHelper); } - public Reader getGroup(String groupId) { + public Reader getGroup(byte[] groupId) { Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, null, GROUP_ID + " = ?", - new String[] {groupId}, null, null, null); + new String[] {GroupUtil.getEncodedId(groupId)}, + null, null, null); return new Reader(cursor); } + public Recipients getGroupMembers(byte[] groupId) { + List members = getCurrentMembers(groupId); + List recipients = new LinkedList(); + + for (String member : members) { + try { + recipients.addAll(RecipientFactory.getRecipientsFromString(context, member, true) + .getRecipientsList()); + } catch (RecipientFormattingException e) { + Log.w("GroupDatabase", e); + } + } + + return new Recipients(recipients); + } + public void create(byte[] groupId, String owner, String title, List members, AttachmentPointer avatar, String relay) { ContentValues contentValues = new ContentValues(); - contentValues.put(GROUP_ID, Hex.toString(groupId)); + contentValues.put(GROUP_ID, GroupUtil.getEncodedId(groupId)); contentValues.put(OWNER, owner); contentValues.put(TITLE, title); contentValues.put(MEMBERS, Util.join(members, ",")); @@ -75,7 +100,7 @@ public class GroupDatabase extends Database { contentValues.put(AVATAR_CONTENT_TYPE, avatar.getContentType()); } - contentValues.put(RELAY, relay); + contentValues.put(AVATAR_RELAY, relay); contentValues.put(TIMESTAMP, System.currentTimeMillis()); databaseHelper.getWritableDatabase().insert(TABLE_NAME, null, contentValues); @@ -93,14 +118,15 @@ public class GroupDatabase extends Database { databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues, GROUP_ID + " = ? AND " + OWNER + " = ?", - new String[] {Hex.toString(groupId), source}); + new String[] {GroupUtil.getEncodedId(groupId), source}); } - public void updateAvatar(String groupId, Bitmap avatar) { + public void updateAvatar(byte[] groupId, Bitmap avatar) { ContentValues contentValues = new ContentValues(); contentValues.put(AVATAR, BitmapUtil.toByteArray(avatar)); - databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues, GROUP_ID + " = ?", new String[] {groupId}); + databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues, GROUP_ID + " = ?", + new String[] {GroupUtil.getEncodedId(groupId)}); } @@ -172,7 +198,7 @@ public class GroupDatabase extends Database { cursor.getLong(cursor.getColumnIndexOrThrow(AVATAR_ID)), cursor.getBlob(cursor.getColumnIndexOrThrow(AVATAR_KEY)), cursor.getString(cursor.getColumnIndexOrThrow(AVATAR_CONTENT_TYPE)), - cursor.getString(cursor.getColumnIndexOrThrow(RELAY))); + cursor.getString(cursor.getColumnIndexOrThrow(AVATAR_RELAY))); } public void close() { @@ -206,8 +232,12 @@ public class GroupDatabase extends Database { this.relay = relay; } - public String getId() { - return id; + public byte[] getId() { + try { + return GroupUtil.getDecodedId(id); + } catch (IOException ioe) { + throw new AssertionError(ioe); + } } public String getTitle() { diff --git a/src/org/thoughtcrime/securesms/database/IdentityDatabase.java b/src/org/thoughtcrime/securesms/database/IdentityDatabase.java index 2602559922..1cda2593f8 100644 --- a/src/org/thoughtcrime/securesms/database/IdentityDatabase.java +++ b/src/org/thoughtcrime/securesms/database/IdentityDatabase.java @@ -67,12 +67,10 @@ public class IdentityDatabase extends Database { } public boolean isValidIdentity(MasterSecret masterSecret, - Recipient recipient, + long recipientId, IdentityKey theirIdentity) { SQLiteDatabase database = databaseHelper.getReadableDatabase(); - String number = recipient.getNumber(); - long recipientId = DatabaseFactory.getAddressDatabase(context).getCanonicalAddress(number); MasterCipher masterCipher = new MasterCipher(masterSecret); Cursor cursor = null; @@ -114,11 +112,9 @@ public class IdentityDatabase extends Database { } } - public void saveIdentity(MasterSecret masterSecret, Recipient recipient, IdentityKey identityKey) + public void saveIdentity(MasterSecret masterSecret, long recipientId, IdentityKey identityKey) { SQLiteDatabase database = databaseHelper.getWritableDatabase(); - String number = recipient.getNumber(); - long recipientId = DatabaseFactory.getAddressDatabase(context).getCanonicalAddress(number); MasterCipher masterCipher = new MasterCipher(masterSecret); String identityKeyString = Base64.encodeBytes(identityKey.serialize()); String macString = Base64.encodeBytes(masterCipher.getMacFor(recipientId + diff --git a/src/org/thoughtcrime/securesms/database/MmsDatabase.java b/src/org/thoughtcrime/securesms/database/MmsDatabase.java index 4a32f76deb..70e81a4f8a 100644 --- a/src/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/src/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -109,6 +109,7 @@ public class MmsDatabase extends Database implements MmsSmsColumns { READ + " INTEGER DEFAULT 0, " + MESSAGE_ID + " TEXT, " + SUBJECT + " TEXT, " + SUBJECT_CHARSET + " INTEGER, " + BODY + " TEXT, " + PART_COUNT + " INTEGER, " + CONTENT_TYPE + " TEXT, " + CONTENT_LOCATION + " TEXT, " + ADDRESS + " TEXT, " + + ADDRESS_DEVICE_ID + " INTEGER, " + EXPIRY + " INTEGER, " + MESSAGE_CLASS + " TEXT, " + MESSAGE_TYPE + " INTEGER, " + MMS_VERSION + " INTEGER, " + MESSAGE_SIZE + " INTEGER, " + PRIORITY + " INTEGER, " + READ_REPORT + " INTEGER, " + REPORT_ALLOWED + " INTEGER, " + RESPONSE_STATUS + " INTEGER, " + @@ -131,7 +132,7 @@ public class MmsDatabase extends Database implements MmsSmsColumns { CONTENT_LOCATION, EXPIRY, MESSAGE_CLASS, MESSAGE_TYPE, MMS_VERSION, MESSAGE_SIZE, PRIORITY, REPORT_ALLOWED, STATUS, TRANSACTION_ID, RETRIEVE_STATUS, RETRIEVE_TEXT, RETRIEVE_TEXT_CS, READ_STATUS, CONTENT_CLASS, RESPONSE_TEXT, - DELIVERY_TIME, DELIVERY_REPORT, BODY, PART_COUNT, ADDRESS + DELIVERY_TIME, DELIVERY_REPORT, BODY, PART_COUNT, ADDRESS, ADDRESS_DEVICE_ID }; public static final ExecutorService slideResolver = org.thoughtcrime.securesms.util.Util.newSingleThreadedLifoExecutor(); @@ -788,6 +789,7 @@ public class MmsDatabase extends Database implements MmsSmsColumns { long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.THREAD_ID)); long mailbox = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.MESSAGE_BOX)); String address = cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.ADDRESS)); + int addressDeviceId = cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.ADDRESS_DEVICE_ID)); Recipients recipients = getRecipientsFor(address); String contentLocation = cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.CONTENT_LOCATION)); @@ -807,8 +809,9 @@ public class MmsDatabase extends Database implements MmsSmsColumns { return new NotificationMmsMessageRecord(context, id, recipients, recipients.getPrimaryRecipient(), - dateSent, dateReceived, threadId, contentLocationBytes, - messageSize, expiry, status, transactionIdBytes, mailbox); + addressDeviceId, dateSent, dateReceived, threadId, + contentLocationBytes, messageSize, expiry, status, + transactionIdBytes, mailbox); } private MediaMmsMessageRecord getMediaMmsMessageRecord(Cursor cursor) { @@ -818,6 +821,7 @@ public class MmsDatabase extends Database implements MmsSmsColumns { long box = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.MESSAGE_BOX)); long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.THREAD_ID)); String address = cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.ADDRESS)); + int addressDeviceId = cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.ADDRESS_DEVICE_ID)); DisplayRecord.Body body = getBody(cursor); int partCount = cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.PART_COUNT)); Recipients recipients = getRecipientsFor(address); @@ -825,29 +829,26 @@ public class MmsDatabase extends Database implements MmsSmsColumns { ListenableFutureTask slideDeck = getSlideDeck(masterSecret, id); return new MediaMmsMessageRecord(context, id, recipients, recipients.getPrimaryRecipient(), - dateSent, dateReceived, threadId, body, + addressDeviceId, dateSent, dateReceived, threadId, body, slideDeck, partCount, box); } private Recipients getRecipientsFor(String address) { try { if (Util.isEmpty(address) || address.equals("insert-address-token")) { - return new Recipients(new Recipient("Unknown", "Unknown", null, - ContactPhotoFactory.getDefaultContactPhoto(context))); + return new Recipients(Recipient.getUnknownRecipient(context)); } Recipients recipients = RecipientFactory.getRecipientsFromString(context, address, false); if (recipients == null || recipients.isEmpty()) { - return new Recipients(new Recipient("Unknown", "Unknown", null, - ContactPhotoFactory.getDefaultContactPhoto(context))); + return new Recipients(Recipient.getUnknownRecipient(context)); } return recipients; } catch (RecipientFormattingException e) { Log.w("MmsDatabase", e); - return new Recipients(new Recipient("Unknown", "Unknown", null, - ContactPhotoFactory.getDefaultContactPhoto(context))); + return new Recipients(Recipient.getUnknownRecipient(context)); } } diff --git a/src/org/thoughtcrime/securesms/database/MmsSmsColumns.java b/src/org/thoughtcrime/securesms/database/MmsSmsColumns.java index 9bdba4565c..99edd838d7 100644 --- a/src/org/thoughtcrime/securesms/database/MmsSmsColumns.java +++ b/src/org/thoughtcrime/securesms/database/MmsSmsColumns.java @@ -8,7 +8,8 @@ public interface MmsSmsColumns { public static final String THREAD_ID = "thread_id"; public static final String READ = "read"; public static final String BODY = "body"; - public static final String ADDRESS = "address"; + public static final String ADDRESS = "address"; + public static final String ADDRESS_DEVICE_ID = "address_device_id"; public static class Types { diff --git a/src/org/thoughtcrime/securesms/database/MmsSmsDatabase.java b/src/org/thoughtcrime/securesms/database/MmsSmsDatabase.java index 97210e6092..f9666decd1 100644 --- a/src/org/thoughtcrime/securesms/database/MmsSmsDatabase.java +++ b/src/org/thoughtcrime/securesms/database/MmsSmsDatabase.java @@ -42,7 +42,7 @@ public class MmsSmsDatabase extends Database { public Cursor getConversation(long threadId) { String[] projection = {MmsSmsColumns.ID, SmsDatabase.BODY, SmsDatabase.TYPE, MmsSmsColumns.THREAD_ID, - SmsDatabase.ADDRESS, SmsDatabase.SUBJECT, + SmsDatabase.ADDRESS, SmsDatabase.ADDRESS_DEVICE_ID, SmsDatabase.SUBJECT, MmsSmsColumns.NORMALIZED_DATE_SENT, MmsSmsColumns.NORMALIZED_DATE_RECEIVED, MmsDatabase.MESSAGE_TYPE, MmsDatabase.MESSAGE_BOX, @@ -64,7 +64,7 @@ public class MmsSmsDatabase extends Database { public Cursor getConversationSnippet(long threadId) { String[] projection = {MmsSmsColumns.ID, SmsDatabase.BODY, SmsDatabase.TYPE, MmsSmsColumns.THREAD_ID, - SmsDatabase.ADDRESS, SmsDatabase.SUBJECT, + SmsDatabase.ADDRESS, SmsDatabase.ADDRESS_DEVICE_ID, SmsDatabase.SUBJECT, MmsSmsColumns.NORMALIZED_DATE_SENT, MmsSmsColumns.NORMALIZED_DATE_RECEIVED, MmsDatabase.MESSAGE_TYPE, MmsDatabase.MESSAGE_BOX, @@ -81,7 +81,7 @@ public class MmsSmsDatabase extends Database { public Cursor getUnread() { String[] projection = {MmsSmsColumns.ID, SmsDatabase.BODY, SmsDatabase.READ, SmsDatabase.TYPE, - SmsDatabase.ADDRESS, SmsDatabase.SUBJECT, MmsSmsColumns.THREAD_ID, + SmsDatabase.ADDRESS, SmsDatabase.ADDRESS_DEVICE_ID, SmsDatabase.SUBJECT, MmsSmsColumns.THREAD_ID, SmsDatabase.STATUS, MmsSmsColumns.NORMALIZED_DATE_SENT, MmsSmsColumns.NORMALIZED_DATE_RECEIVED, @@ -108,7 +108,7 @@ public class MmsSmsDatabase extends Database { String[] mmsProjection = {MmsDatabase.DATE_SENT + " * 1000 AS " + MmsSmsColumns.NORMALIZED_DATE_SENT, MmsDatabase.DATE_RECEIVED + " * 1000 AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED, MmsSmsColumns.ID, SmsDatabase.BODY, MmsSmsColumns.READ, MmsSmsColumns.THREAD_ID, - SmsDatabase.TYPE, SmsDatabase.ADDRESS, SmsDatabase.SUBJECT, MmsDatabase.MESSAGE_TYPE, + SmsDatabase.TYPE, SmsDatabase.ADDRESS, SmsDatabase.ADDRESS_DEVICE_ID, SmsDatabase.SUBJECT, MmsDatabase.MESSAGE_TYPE, MmsDatabase.MESSAGE_BOX, SmsDatabase.STATUS, MmsDatabase.PART_COUNT, MmsDatabase.CONTENT_LOCATION, MmsDatabase.TRANSACTION_ID, MmsDatabase.MESSAGE_SIZE, MmsDatabase.EXPIRY, MmsDatabase.STATUS, @@ -117,7 +117,7 @@ public class MmsSmsDatabase extends Database { String[] smsProjection = {SmsDatabase.DATE_SENT + " * 1 AS " + MmsSmsColumns.NORMALIZED_DATE_SENT, SmsDatabase.DATE_RECEIVED + " * 1 AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED, MmsSmsColumns.ID, SmsDatabase.BODY, MmsSmsColumns.READ, MmsSmsColumns.THREAD_ID, - SmsDatabase.TYPE, SmsDatabase.ADDRESS, SmsDatabase.SUBJECT, MmsDatabase.MESSAGE_TYPE, + SmsDatabase.TYPE, SmsDatabase.ADDRESS, SmsDatabase.ADDRESS_DEVICE_ID, SmsDatabase.SUBJECT, MmsDatabase.MESSAGE_TYPE, MmsDatabase.MESSAGE_BOX, SmsDatabase.STATUS, MmsDatabase.PART_COUNT, MmsDatabase.CONTENT_LOCATION, MmsDatabase.TRANSACTION_ID, MmsDatabase.MESSAGE_SIZE, MmsDatabase.EXPIRY, MmsDatabase.STATUS, @@ -139,6 +139,7 @@ public class MmsSmsDatabase extends Database { mmsColumnsPresent.add(MmsSmsColumns.THREAD_ID); mmsColumnsPresent.add(MmsSmsColumns.BODY); mmsColumnsPresent.add(MmsSmsColumns.ADDRESS); + mmsColumnsPresent.add(MmsSmsColumns.ADDRESS_DEVICE_ID); mmsColumnsPresent.add(MmsDatabase.MESSAGE_TYPE); mmsColumnsPresent.add(MmsDatabase.MESSAGE_BOX); mmsColumnsPresent.add(MmsDatabase.DATE_SENT); @@ -154,6 +155,7 @@ public class MmsSmsDatabase extends Database { smsColumnsPresent.add(MmsSmsColumns.ID); smsColumnsPresent.add(MmsSmsColumns.BODY); smsColumnsPresent.add(MmsSmsColumns.ADDRESS); + smsColumnsPresent.add(MmsSmsColumns.ADDRESS_DEVICE_ID); smsColumnsPresent.add(MmsSmsColumns.READ); smsColumnsPresent.add(MmsSmsColumns.THREAD_ID); smsColumnsPresent.add(SmsDatabase.TYPE); diff --git a/src/org/thoughtcrime/securesms/database/PushDatabase.java b/src/org/thoughtcrime/securesms/database/PushDatabase.java index 8df5ec7a3e..a4f6552a34 100644 --- a/src/org/thoughtcrime/securesms/database/PushDatabase.java +++ b/src/org/thoughtcrime/securesms/database/PushDatabase.java @@ -18,11 +18,12 @@ public class PushDatabase extends Database { public static final String ID = "_id"; public static final String TYPE = "type"; public static final String SOURCE = "source"; + public static final String DEVICE_ID = "device_id"; public static final String BODY = "body"; public static final String TIMESTAMP = "timestamp"; public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " + - TYPE + " INTEGER, " + SOURCE + " TEXT, " + BODY + " TEXT, " + TIMESTAMP + " INTEGER);"; + TYPE + " INTEGER, " + SOURCE + " TEXT, " + DEVICE_ID + " INTEGER, " + BODY + " TEXT, " + TIMESTAMP + " INTEGER);"; public PushDatabase(Context context, SQLiteOpenHelper databaseHelper) { super(context, databaseHelper); @@ -64,10 +65,11 @@ public class PushDatabase extends Database { int type = cursor.getInt(cursor.getColumnIndexOrThrow(TYPE)); String source = cursor.getString(cursor.getColumnIndexOrThrow(SOURCE)); + int deviceId = cursor.getInt(cursor.getColumnIndexOrThrow(DEVICE_ID)); byte[] body = Base64.decode(cursor.getString(cursor.getColumnIndexOrThrow(BODY))); long timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(TIMESTAMP)); - return new IncomingPushMessage(type, source, body, timestamp); + return new IncomingPushMessage(type, source, deviceId, body, timestamp); } catch (IOException e) { throw new AssertionError(e); } diff --git a/src/org/thoughtcrime/securesms/database/SmsDatabase.java b/src/org/thoughtcrime/securesms/database/SmsDatabase.java index 46ca52a280..5beacd9591 100644 --- a/src/org/thoughtcrime/securesms/database/SmsDatabase.java +++ b/src/org/thoughtcrime/securesms/database/SmsDatabase.java @@ -63,8 +63,8 @@ public class SmsDatabase extends Database implements MmsSmsColumns { public static final String SERVICE_CENTER = "service_center"; public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " integer PRIMARY KEY, " + - THREAD_ID + " INTEGER, " + ADDRESS + " TEXT, " + PERSON + " INTEGER, " + DATE_RECEIVED + " INTEGER, " + - DATE_SENT + " INTEGER, " + PROTOCOL + " INTEGER, " + READ + " INTEGER DEFAULT 0, " + + THREAD_ID + " INTEGER, " + ADDRESS + " TEXT, " + ADDRESS_DEVICE_ID + " INTEGER DEFAULT 1, " + PERSON + " INTEGER, " + + DATE_RECEIVED + " INTEGER, " + DATE_SENT + " INTEGER, " + PROTOCOL + " INTEGER, " + READ + " INTEGER DEFAULT 0, " + STATUS + " INTEGER DEFAULT -1," + TYPE + " INTEGER, " + REPLY_PATH_PRESENT + " INTEGER, " + SUBJECT + " TEXT, " + BODY + " TEXT, " + SERVICE_CENTER + " TEXT);"; @@ -76,7 +76,7 @@ public class SmsDatabase extends Database implements MmsSmsColumns { }; private static final String[] MESSAGE_PROJECTION = new String[] { - ID, THREAD_ID, ADDRESS, PERSON, + ID, THREAD_ID, ADDRESS, ADDRESS_DEVICE_ID, PERSON, DATE_RECEIVED + " AS " + NORMALIZED_DATE_RECEIVED, DATE_SENT + " AS " + NORMALIZED_DATE_SENT, PROTOCOL, READ, STATUS, TYPE, @@ -257,19 +257,39 @@ public class SmsDatabase extends Database implements MmsSmsColumns { type |= Types.ENCRYPTION_REMOTE_BIT; } - Recipient recipient = new Recipient(null, message.getSender(), null, null); - Recipients recipients = new Recipients(recipient); - String groupId = message.getGroupId(); + Recipients recipients; + + try { + recipients = RecipientFactory.getRecipientsFromString(context, message.getSender(), true); + } catch (RecipientFormattingException e) { + Log.w("SmsDatabase", e); + recipients = new Recipients(Recipient.getUnknownRecipient(context)); + } + + Recipients groupRecipients; + + try { + if (message.getGroupId() == null) { + groupRecipients = null; + } else { + groupRecipients = RecipientFactory.getRecipientsFromString(context, message.getGroupId(), true); + } + } catch (RecipientFormattingException e) { + Log.w("SmsDatabase", e); + groupRecipients = null; + } + boolean unread = org.thoughtcrime.securesms.util.Util.isDefaultSmsProvider(context) || message.isSecureMessage() || message.isKeyExchange(); long threadId; - if (groupId == null) threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipients); - else threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdForGroup(groupId); + if (groupRecipients == null) threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipients); + else threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipients); ContentValues values = new ContentValues(6); values.put(ADDRESS, message.getSender()); + values.put(ADDRESS_DEVICE_ID, message.getSenderDeviceId()); values.put(DATE_RECEIVED, System.currentTimeMillis()); values.put(DATE_SENT, message.getSentTimestampMillis()); values.put(PROTOCOL, message.getProtocol()); @@ -468,6 +488,7 @@ public class SmsDatabase extends Database implements MmsSmsColumns { public SmsMessageRecord getCurrent() { long messageId = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.ID)); String address = cursor.getString(cursor.getColumnIndexOrThrow(SmsDatabase.ADDRESS)); + int addressDeviceId = cursor.getInt(cursor.getColumnIndexOrThrow(SmsDatabase.ADDRESS_DEVICE_ID)); long type = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.TYPE)); long dateReceived = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.NORMALIZED_DATE_RECEIVED)); long dateSent = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.NORMALIZED_DATE_SENT)); @@ -478,6 +499,7 @@ public class SmsDatabase extends Database implements MmsSmsColumns { return new SmsMessageRecord(context, messageId, body, recipients, recipients.getPrimaryRecipient(), + addressDeviceId, dateSent, dateReceived, type, threadId, status); } @@ -487,15 +509,13 @@ public class SmsDatabase extends Database implements MmsSmsColumns { Recipients recipients = RecipientFactory.getRecipientsFromString(context, address, false); if (recipients == null || recipients.isEmpty()) { - return new Recipients(new Recipient("Unknown", "Unknown", null, - ContactPhotoFactory.getDefaultContactPhoto(context))); + return new Recipients(Recipient.getUnknownRecipient(context)); } return recipients; } catch (RecipientFormattingException e) { Log.w("EncryptingSmsDatabase", e); - return new Recipients(new Recipient("Unknown", "Unknown", null, - ContactPhotoFactory.getDefaultContactPhoto(context))); + return new Recipients(Recipient.getUnknownRecipient(context)); } } diff --git a/src/org/thoughtcrime/securesms/database/ThreadDatabase.java b/src/org/thoughtcrime/securesms/database/ThreadDatabase.java index ec124ace47..0aa8842192 100644 --- a/src/org/thoughtcrime/securesms/database/ThreadDatabase.java +++ b/src/org/thoughtcrime/securesms/database/ThreadDatabase.java @@ -70,13 +70,11 @@ public class ThreadDatabase extends Database { } private long[] getRecipientIds(Recipients recipients) { - Set recipientSet = new HashSet(); + Set recipientSet = new HashSet(); List recipientList = recipients.getRecipientsList(); for (Recipient recipient : recipientList) { - // String number = NumberUtil.filterNumber(recipient.getNumber()); - String number = recipient.getNumber(); - recipientSet.add(Long.valueOf(DatabaseFactory.getAddressDatabase(context).getCanonicalAddress(number))); + recipientSet.add(recipient.getRecipientId()); } long[] recipientArray = new long[recipientSet.size()]; diff --git a/src/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java b/src/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java index 8d0c5a3c95..46fac48fbd 100644 --- a/src/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java +++ b/src/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java @@ -41,12 +41,13 @@ public class MediaMmsMessageRecord extends MessageRecord { private final ListenableFutureTask slideDeck; public MediaMmsMessageRecord(Context context, long id, Recipients recipients, - Recipient individualRecipient, long dateSent, long dateReceived, - long threadId, Body body, ListenableFutureTask slideDeck, + Recipient individualRecipient, int recipientDeviceId, + long dateSent, long dateReceived, long threadId, Body body, + ListenableFutureTask slideDeck, int partCount, long mailbox) { - super(context, id, body, recipients, individualRecipient, dateSent, dateReceived, - threadId, DELIVERY_STATUS_NONE, mailbox); + super(context, id, body, recipients, individualRecipient, recipientDeviceId, + dateSent, dateReceived, threadId, DELIVERY_STATUS_NONE, mailbox); this.context = context.getApplicationContext(); this.partCount = partCount; diff --git a/src/org/thoughtcrime/securesms/database/model/MessageRecord.java b/src/org/thoughtcrime/securesms/database/model/MessageRecord.java index be07a43698..724fa88939 100644 --- a/src/org/thoughtcrime/securesms/database/model/MessageRecord.java +++ b/src/org/thoughtcrime/securesms/database/model/MessageRecord.java @@ -45,18 +45,20 @@ public abstract class MessageRecord extends DisplayRecord { public static final int DELIVERY_STATUS_FAILED = 3; private final Recipient individualRecipient; - private final long id; - private final int deliveryStatus; + private final int recipientDeviceId; + private final long id; + private final int deliveryStatus; - public MessageRecord(Context context, long id, Body body, Recipients recipients, - Recipient individualRecipient, - long dateSent, long dateReceived, - long threadId, int deliveryStatus, - long type) + MessageRecord(Context context, long id, Body body, Recipients recipients, + Recipient individualRecipient, int recipientDeviceId, + long dateSent, long dateReceived, + long threadId, int deliveryStatus, + long type) { super(context, body, recipients, dateSent, dateReceived, threadId, type); this.id = id; this.individualRecipient = individualRecipient; + this.recipientDeviceId = recipientDeviceId; this.deliveryStatus = deliveryStatus; } @@ -121,6 +123,10 @@ public abstract class MessageRecord extends DisplayRecord { return individualRecipient; } + public int getRecipientDeviceId() { + return recipientDeviceId; + } + public long getType() { return type; } diff --git a/src/org/thoughtcrime/securesms/database/model/NotificationMmsMessageRecord.java b/src/org/thoughtcrime/securesms/database/model/NotificationMmsMessageRecord.java index 99039e773f..7ebf70c75d 100644 --- a/src/org/thoughtcrime/securesms/database/model/NotificationMmsMessageRecord.java +++ b/src/org/thoughtcrime/securesms/database/model/NotificationMmsMessageRecord.java @@ -41,13 +41,13 @@ public class NotificationMmsMessageRecord extends MessageRecord { private final byte[] transactionId; public NotificationMmsMessageRecord(Context context, long id, Recipients recipients, - Recipient individualRecipient, + Recipient individualRecipient, int recipientDeviceId, long dateSent, long dateReceived, long threadId, byte[] contentLocation, long messageSize, long expiry, int status, byte[] transactionId, long mailbox) { - super(context, id, new Body("", true), recipients, individualRecipient, dateSent, dateReceived, - threadId, DELIVERY_STATUS_NONE, mailbox); + super(context, id, new Body("", true), recipients, individualRecipient, recipientDeviceId, + dateSent, dateReceived, threadId, DELIVERY_STATUS_NONE, mailbox); this.contentLocation = contentLocation; this.messageSize = messageSize; diff --git a/src/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java b/src/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java index 5fa9eaec98..e630507e96 100644 --- a/src/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java +++ b/src/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java @@ -38,12 +38,13 @@ public class SmsMessageRecord extends MessageRecord { public SmsMessageRecord(Context context, long id, Body body, Recipients recipients, Recipient individualRecipient, + int recipientDeviceId, long dateSent, long dateReceived, long type, long threadId, int status) { - super(context, id, body, recipients, individualRecipient, dateSent, dateReceived, - threadId, getGenericDeliveryStatus(status), type); + super(context, id, body, recipients, individualRecipient, recipientDeviceId, + dateSent, dateReceived, threadId, getGenericDeliveryStatus(status), type); } public long getType() { diff --git a/src/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java b/src/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java index 9b66b822f3..1fc0b2ea81 100644 --- a/src/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java +++ b/src/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java @@ -1,11 +1,13 @@ package org.thoughtcrime.securesms.mms; +import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.Util; import org.whispersystems.textsecure.crypto.MasterCipher; import org.whispersystems.textsecure.crypto.MasterSecret; import org.whispersystems.textsecure.push.IncomingPushMessage; import org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent; import org.whispersystems.textsecure.util.Base64; +import org.whispersystems.textsecure.util.Hex; import ws.com.google.android.mms.pdu.EncodedStringValue; import ws.com.google.android.mms.pdu.PduBody; @@ -27,12 +29,16 @@ public class IncomingMediaMessage { public IncomingMediaMessage(MasterSecret masterSecret, String localNumber, IncomingPushMessage message, - PushMessageContent messageContent, - String groupId) + PushMessageContent messageContent) { this.headers = new PduHeaders(); this.body = new PduBody(); - this.groupId = groupId; + + if (messageContent.hasGroup()) { + this.groupId = GroupUtil.getEncodedId(messageContent.getGroup().getId().toByteArray()); + } else { + this.groupId = null; + } this.headers.setEncodedStringValue(new EncodedStringValue(message.getSource()), PduHeaders.FROM); this.headers.appendEncodedStringValue(new EncodedStringValue(localNumber), PduHeaders.TO); diff --git a/src/org/thoughtcrime/securesms/notifications/MessageNotifier.java b/src/org/thoughtcrime/securesms/notifications/MessageNotifier.java index cc4fa6786e..44b06da4a6 100644 --- a/src/org/thoughtcrime/securesms/notifications/MessageNotifier.java +++ b/src/org/thoughtcrime/securesms/notifications/MessageNotifier.java @@ -296,7 +296,7 @@ public class MessageNotifier { recipient = RecipientFactory.getRecipientsFromString(context, message.getSource(), false).getPrimaryRecipient(); } catch (RecipientFormattingException e) { Log.w("MessageNotifier", e); - recipient = new Recipient("Unknown", "Unknown", null, ContactPhotoFactory.getDefaultContactPhoto(context)); + recipient = Recipient.getUnknownRecipient(context); } Recipients recipients = RecipientFactory.getRecipientsFromMessage(context, message, false); diff --git a/src/org/thoughtcrime/securesms/recipients/Recipient.java b/src/org/thoughtcrime/securesms/recipients/Recipient.java index 9715f429f0..634367e6a8 100644 --- a/src/org/thoughtcrime/securesms/recipients/Recipient.java +++ b/src/org/thoughtcrime/securesms/recipients/Recipient.java @@ -23,15 +23,16 @@ import android.os.Parcel; import android.os.Parcelable; import android.util.Log; -import org.thoughtcrime.securesms.database.CanonicalAddressDatabase; +import org.thoughtcrime.securesms.contacts.ContactPhotoFactory; import org.thoughtcrime.securesms.recipients.RecipientProvider.RecipientDetails; +import org.thoughtcrime.securesms.util.GroupUtil; +import org.whispersystems.textsecure.storage.CanonicalRecipient; import org.whispersystems.textsecure.util.FutureTaskListener; import org.whispersystems.textsecure.util.ListenableFutureTask; -import org.whispersystems.textsecure.storage.CanonicalRecipientAddress; import java.util.HashSet; -public class Recipient implements Parcelable, CanonicalRecipientAddress { +public class Recipient implements Parcelable, CanonicalRecipient { public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { public Recipient createFromParcel(Parcel in) { @@ -43,18 +44,21 @@ public class Recipient implements Parcelable, CanonicalRecipientAddress { } }; - private final String number; private final HashSet listeners = new HashSet(); + private final String number; + private final long recipientId; + private String name; private Bitmap contactPhoto; private Uri contactUri; - public Recipient(String number, Bitmap contactPhoto, - ListenableFutureTask future) + Recipient(String number, Bitmap contactPhoto, long recipientId, + ListenableFutureTask future) { this.number = number; this.contactPhoto = contactPhoto; + this.recipientId = recipientId; future.setListener(new FutureTaskListener() { @Override @@ -82,8 +86,9 @@ public class Recipient implements Parcelable, CanonicalRecipientAddress { }); } - public Recipient(String name, String number, Uri contactUri, Bitmap contactPhoto) { + Recipient(String name, String number, long recipientId, Uri contactUri, Bitmap contactPhoto) { this.number = number; + this.recipientId = recipientId; this.contactUri = contactUri; this.name = name; this.contactPhoto = contactPhoto; @@ -92,6 +97,7 @@ public class Recipient implements Parcelable, CanonicalRecipientAddress { public Recipient(Parcel in) { this.number = in.readString(); this.name = in.readString(); + this.recipientId = in.readLong(); this.contactUri = (Uri)in.readParcelable(null); this.contactPhoto = (Bitmap)in.readParcelable(null); } @@ -112,6 +118,14 @@ public class Recipient implements Parcelable, CanonicalRecipientAddress { return 0; } + public long getRecipientId() { + return recipientId; + } + + public boolean isGroupRecipient() { + return GroupUtil.isEncodedGroup(number); + } + // public void updateAsynchronousContent(RecipientDetails result) { // if (result != null) { // Recipient.this.name.set(result.name); @@ -136,6 +150,7 @@ public class Recipient implements Parcelable, CanonicalRecipientAddress { public synchronized void writeToParcel(Parcel dest, int flags) { dest.writeString(number); dest.writeString(name); + dest.writeLong(recipientId); dest.writeParcelable(contactUri, 0); dest.writeParcelable(contactPhoto, 0); } @@ -148,11 +163,12 @@ public class Recipient implements Parcelable, CanonicalRecipientAddress { return contactPhoto; } - public long getCanonicalAddress(Context context) { - return CanonicalAddressDatabase.getInstance(context).getCanonicalAddress(getNumber()); + public static Recipient getUnknownRecipient(Context context) { + return new Recipient("Unknown", "Unknown", -1, null, ContactPhotoFactory.getDefaultContactPhoto(context)); } public static interface RecipientModifiedListener { public void onModified(Recipient recipient); } + } diff --git a/src/org/thoughtcrime/securesms/recipients/RecipientFactory.java b/src/org/thoughtcrime/securesms/recipients/RecipientFactory.java index 836b6a20f5..aac8f8a172 100644 --- a/src/org/thoughtcrime/securesms/recipients/RecipientFactory.java +++ b/src/org/thoughtcrime/securesms/recipients/RecipientFactory.java @@ -20,6 +20,7 @@ import android.content.Context; import android.util.Log; import org.thoughtcrime.securesms.contacts.ContactPhotoFactory; +import org.thoughtcrime.securesms.database.CanonicalAddressDatabase; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.util.NumberUtil; import org.whispersystems.textsecure.push.IncomingPushMessage; @@ -51,7 +52,8 @@ public class RecipientFactory { } private static Recipient getRecipientForNumber(Context context, String number, boolean asynchronous) { - return provider.getRecipient(context, number, asynchronous); + long recipientId = CanonicalAddressDatabase.getInstance(context).getCanonicalAddress(number); + return provider.getRecipient(context, recipientId, asynchronous); } public static Recipients getRecipientsFromString(Context context, String rawText, boolean asynchronous) @@ -81,17 +83,16 @@ public class RecipientFactory { return getRecipientsFromString(context, message.getSource(), asynchronous); } catch (RecipientFormattingException e) { Log.w("RecipientFactory", e); - return new Recipients(new Recipient("Unknown", "Unknown", null, - ContactPhotoFactory.getDefaultContactPhoto(context))); + return new Recipients(Recipient.getUnknownRecipient(context)); } } private static Recipient getRecipientFromProviderId(Context context, String recipientId, boolean asynchronous) { - if (recipientId.startsWith("g_")) { - return provider.getGroupRecipient(context, recipientId, asynchronous); - } else { - String number = DatabaseFactory.getAddressDatabase(context).getAddressFromId(recipientId); - return getRecipientForNumber(context, number, asynchronous); + try { + return provider.getRecipient(context, Long.parseLong(recipientId), asynchronous); + } catch (NumberFormatException e) { + Log.w("RecipientFactory", e); + return Recipient.getUnknownRecipient(context); } } @@ -126,7 +127,7 @@ public class RecipientFactory { if (hasBracketedNumber(recipient)) return getRecipientForNumber(context, parseBracketedNumber(recipient), asynchronous); - if (NumberUtil.isValidSmsOrEmail(recipient)) + if (NumberUtil.isValidSmsOrEmailOrGroup(recipient)) return getRecipientForNumber(context, recipient, asynchronous); throw new RecipientFormattingException("Recipient: " + recipient + " is badly formatted."); diff --git a/src/org/thoughtcrime/securesms/recipients/RecipientProvider.java b/src/org/thoughtcrime/securesms/recipients/RecipientProvider.java index 149923caae..ab2c78e68e 100644 --- a/src/org/thoughtcrime/securesms/recipients/RecipientProvider.java +++ b/src/org/thoughtcrime/securesms/recipients/RecipientProvider.java @@ -27,12 +27,15 @@ import android.provider.ContactsContract.PhoneLookup; import android.util.Log; import org.thoughtcrime.securesms.contacts.ContactPhotoFactory; +import org.thoughtcrime.securesms.database.CanonicalAddressDatabase; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.GroupDatabase; +import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.LRUCache; import org.whispersystems.textsecure.util.ListenableFutureTask; import org.thoughtcrime.securesms.util.Util; +import java.io.IOException; import java.io.InputStream; import java.util.Collections; import java.util.Map; @@ -41,8 +44,8 @@ import java.util.concurrent.ExecutorService; public class RecipientProvider { - private static final Map recipientCache = Collections.synchronizedMap(new LRUCache(1000)); - private static final ExecutorService asyncRecipientResolver = Util.newSingleThreadedLifoExecutor(); + private static final Map recipientCache = Collections.synchronizedMap(new LRUCache(1000)); + private static final ExecutorService asyncRecipientResolver = Util.newSingleThreadedLifoExecutor(); private static final String[] CALLER_ID_PROJECTION = new String[] { PhoneLookup.DISPLAY_NAME, @@ -50,58 +53,45 @@ public class RecipientProvider { PhoneLookup._ID, }; - public Recipient getRecipient(Context context, String number, boolean asynchronous) { - Recipient cachedRecipient = recipientCache.get(number); + public Recipient getRecipient(Context context, long recipientId, boolean asynchronous) { + Recipient cachedRecipient = recipientCache.get(recipientId); if (cachedRecipient != null) return cachedRecipient; - else if (asynchronous) return getAsynchronousRecipient(context, number); - else return getSynchronousRecipient(context, number); + else if (asynchronous) return getAsynchronousRecipient(context, recipientId); + else return getSynchronousRecipient(context, recipientId); } - public Recipient getGroupRecipient(Context context, String groupId, boolean asynchronous) { - Recipient cachedRecipient = recipientCache.get(groupId); - - if (cachedRecipient != null) return cachedRecipient; - else if (asynchronous) return getAsynchronousGroupRecipient(context, groupId); - else return getSynchronousGroupRecipient(context, groupId); - } - - private Recipient getSynchronousRecipient(Context context, String number) { + private Recipient getSynchronousRecipient(Context context, long recipientId) { Log.w("RecipientProvider", "Cache miss [SYNC]!"); - RecipientDetails details = getRecipientDetails(context, number); + Recipient recipient; + RecipientDetails details; + + String number = CanonicalAddressDatabase.getInstance(context).getAddressFromId(String.valueOf(recipientId)); + + if (GroupUtil.isEncodedGroup(number)) details = getGroupRecipientDetails(context, number); + else details = getRecipientDetails(context, number); if (details != null) { - recipient = new Recipient(details.name, number, details.contactUri, details.avatar); + recipient = new Recipient(details.name, number, recipientId, details.contactUri, details.avatar); } else { - recipient = new Recipient(null, number, null, ContactPhotoFactory.getDefaultContactPhoto(context)); + recipient = new Recipient(null, number, recipientId, null, ContactPhotoFactory.getDefaultContactPhoto(context)); } - recipientCache.put(number, recipient); + recipientCache.put(recipientId, recipient); return recipient; } - private Recipient getSynchronousGroupRecipient(Context context, String groupId) { - RecipientDetails details = getGroupRecipientDetails(context, groupId); - Recipient recipient; - - if (details != null) { - recipient = new Recipient(details.name, groupId, details.contactUri, details.avatar); - } else { - recipient = new Recipient(null, groupId, null, ContactPhotoFactory.getDefaultContactPhoto(context)); - } - - recipientCache.put(groupId, recipient); - return recipient; - } - - private Recipient getAsynchronousRecipient(final Context context, final String number) { + private Recipient getAsynchronousRecipient(final Context context, final long recipientId) { Log.w("RecipientProvider", "Cache miss [ASYNC]!"); + final String number = CanonicalAddressDatabase.getInstance(context).getAddressFromId(String.valueOf(recipientId)); + Callable task = new Callable() { @Override public RecipientDetails call() throws Exception { - return getRecipientDetails(context, number); + if (GroupUtil.isEncodedGroup(number)) return getGroupRecipientDetails(context, number); + else return getRecipientDetails(context, number); } }; @@ -109,26 +99,8 @@ public class RecipientProvider { asyncRecipientResolver.submit(future); - Recipient recipient = new Recipient(number, ContactPhotoFactory.getDefaultContactPhoto(context), future); - recipientCache.put(number, recipient); - - return recipient; - } - - private Recipient getAsynchronousGroupRecipient(final Context context, final String groupId) { - Callable task = new Callable() { - @Override - public RecipientDetails call() throws Exception { - return getGroupRecipientDetails(context, groupId); - } - }; - - ListenableFutureTask future = new ListenableFutureTask(task, null); - - asyncRecipientResolver.submit(future); - - Recipient recipient = new Recipient(groupId, ContactPhotoFactory.getDefaultContactPhoto(context), future); - recipientCache.put(groupId, recipient); + Recipient recipient = new Recipient(number, ContactPhotoFactory.getDefaultContactPhoto(context), recipientId, future); + recipientCache.put(recipientId, recipient); return recipient; } @@ -159,24 +131,31 @@ public class RecipientProvider { } private RecipientDetails getGroupRecipientDetails(Context context, String groupId) { - GroupDatabase.Reader reader = DatabaseFactory.getGroupDatabase(context).getGroup(groupId.substring(2)); - GroupDatabase.GroupRecord record; - try { - if ((record = reader.getNext()) != null) { - byte[] avatarBytes = record.getAvatar(); - Bitmap avatar; + GroupDatabase.Reader reader = DatabaseFactory.getGroupDatabase(context) + .getGroup(GroupUtil.getDecodedId(groupId)); - if (avatarBytes == null) avatar = ContactPhotoFactory.getDefaultContactPhoto(context); - else avatar = BitmapFactory.decodeByteArray(avatarBytes, 0, avatarBytes.length); + GroupDatabase.GroupRecord record; - return new RecipientDetails(record.getTitle(), null, avatar); + try { + if ((record = reader.getNext()) != null) { + byte[] avatarBytes = record.getAvatar(); + Bitmap avatar; + + if (avatarBytes == null) avatar = ContactPhotoFactory.getDefaultContactPhoto(context); + else avatar = BitmapFactory.decodeByteArray(avatarBytes, 0, avatarBytes.length); + + return new RecipientDetails(record.getTitle(), null, avatar); + } + } finally { + reader.close(); } - } finally { - reader.close(); - } - return null; + return null; + } catch (IOException e) { + Log.w("RecipientProvider", e); + return null; + } } private Bitmap getContactPhoto(Context context, Uri uri) { diff --git a/src/org/thoughtcrime/securesms/recipients/Recipients.java b/src/org/thoughtcrime/securesms/recipients/Recipients.java index 7ea7a56dff..60857f9d70 100644 --- a/src/org/thoughtcrime/securesms/recipients/Recipients.java +++ b/src/org/thoughtcrime/securesms/recipients/Recipients.java @@ -21,6 +21,7 @@ import android.os.Parcelable; import android.util.Patterns; import org.thoughtcrime.securesms.recipients.Recipient.RecipientModifiedListener; +import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.NumberUtil; import java.util.ArrayList; @@ -89,6 +90,10 @@ public class Recipients implements Parcelable { return false; } + public boolean isGroupRecipient() { + return isSingleRecipient() && GroupUtil.isEncodedGroup(recipients.get(0).getNumber()); + } + // public Recipients getSecureSessionRecipients(Context context) { // List secureRecipients = new LinkedList(); // diff --git a/src/org/thoughtcrime/securesms/service/AvatarDownloader.java b/src/org/thoughtcrime/securesms/service/AvatarDownloader.java index 0e467ff967..8d5c30f7f5 100644 --- a/src/org/thoughtcrime/securesms/service/AvatarDownloader.java +++ b/src/org/thoughtcrime/securesms/service/AvatarDownloader.java @@ -33,7 +33,7 @@ public class AvatarDownloader { if (!SendReceiveService.DOWNLOAD_AVATAR_ACTION.equals(intent.getAction())) return; - String groupId = intent.getStringExtra("group_id"); + byte[] groupId = intent.getByteArrayExtra("group_id"); GroupDatabase database = DatabaseFactory.getGroupDatabase(context); GroupDatabase.Reader reader = database.getGroup(groupId); diff --git a/src/org/thoughtcrime/securesms/service/PushReceiver.java b/src/org/thoughtcrime/securesms/service/PushReceiver.java index 7ee7471670..307bd29b2e 100644 --- a/src/org/thoughtcrime/securesms/service/PushReceiver.java +++ b/src/org/thoughtcrime/securesms/service/PushReceiver.java @@ -17,12 +17,14 @@ import org.thoughtcrime.securesms.mms.IncomingMediaMessage; import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientFactory; +import org.thoughtcrime.securesms.recipients.RecipientFormattingException; import org.thoughtcrime.securesms.recipients.Recipients; import org.thoughtcrime.securesms.sms.IncomingEncryptedMessage; import org.thoughtcrime.securesms.sms.IncomingKeyExchangeMessage; import org.thoughtcrime.securesms.sms.IncomingPreKeyBundleMessage; import org.thoughtcrime.securesms.sms.IncomingTextMessage; import org.thoughtcrime.securesms.sms.SmsTransportDetails; +import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.textsecure.crypto.InvalidKeyException; import org.whispersystems.textsecure.crypto.InvalidMessageException; @@ -32,6 +34,7 @@ import org.whispersystems.textsecure.crypto.protocol.PreKeyWhisperMessage; import org.whispersystems.textsecure.push.IncomingPushMessage; import org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent; import org.whispersystems.textsecure.storage.InvalidKeyIdException; +import org.whispersystems.textsecure.storage.RecipientDevice; import org.whispersystems.textsecure.util.Hex; import ws.com.google.android.mms.MmsException; @@ -106,9 +109,10 @@ public class PushReceiver { } try { - Recipient recipient = new Recipient(null, message.getSource(), null, null); - KeyExchangeProcessorV2 processor = new KeyExchangeProcessorV2(context, masterSecret, recipient); - PreKeyWhisperMessage preKeyExchange = new PreKeyWhisperMessage(message.getBody()); + Recipient recipient = RecipientFactory.getRecipientsFromString(context, message.getSource(), false).getPrimaryRecipient(); + RecipientDevice recipientDevice = new RecipientDevice(recipient.getRecipientId(), message.getSourceDevice()); + KeyExchangeProcessorV2 processor = new KeyExchangeProcessorV2(context, masterSecret, recipientDevice); + PreKeyWhisperMessage preKeyExchange = new PreKeyWhisperMessage(message.getBody()); if (processor.isTrusted(preKeyExchange)) { processor.processKeyExchangeMessage(preKeyExchange); @@ -135,6 +139,9 @@ public class PushReceiver { } catch (InvalidMessageException e) { Log.w("PushReceiver", e); handleReceivedCorruptedKey(masterSecret, message, false); + } catch (RecipientFormattingException e) { + Log.w("PushReceiver", e); + handleReceivedCorruptedKey(masterSecret, message, false); } } @@ -151,10 +158,10 @@ public class PushReceiver { handleReceivedGroupMessage(masterSecret, message, messageContent, secure); } else if (messageContent.getAttachmentsCount() > 0) { Log.w("PushReceiver", "Received push media message..."); - handleReceivedMediaMessage(masterSecret, message, messageContent, secure, null); + handleReceivedMediaMessage(masterSecret, message, messageContent, secure); } else { Log.w("PushReceiver", "Received push text message..."); - handleReceivedTextMessage(masterSecret, message, messageContent, secure, null); + handleReceivedTextMessage(masterSecret, message, messageContent, secure); } } catch (InvalidProtocolBufferException e) { Log.w("PushReceiver", e); @@ -207,30 +214,28 @@ public class PushReceiver { if (group.hasAvatar()) { Intent intent = new Intent(context, SendReceiveService.class); intent.setAction(SendReceiveService.DOWNLOAD_AVATAR_ACTION); + intent.putExtra("group_id", group.getId().toByteArray()); context.startService(intent); } - String groupId = "g_" + Hex.toString(group.getId().toByteArray()); - if (messageContent.getAttachmentsCount() > 0) { - handleReceivedMediaMessage(masterSecret, message, messageContent, secure, groupId); + handleReceivedMediaMessage(masterSecret, message, messageContent, secure); } else if (messageContent.hasBody()) { - handleReceivedTextMessage(masterSecret, message, messageContent, secure, groupId); + handleReceivedTextMessage(masterSecret, message, messageContent, secure); } } private void handleReceivedMediaMessage(MasterSecret masterSecret, IncomingPushMessage message, PushMessageContent messageContent, - boolean secure, String groupId) + boolean secure) { try { String localNumber = TextSecurePreferences.getLocalNumber(context); MmsDatabase database = DatabaseFactory.getMmsDatabase(context); IncomingMediaMessage mediaMessage = new IncomingMediaMessage(masterSecret, localNumber, - message, messageContent, - groupId); + message, messageContent); Pair messageAndThreadId; @@ -255,9 +260,10 @@ public class PushReceiver { private void handleReceivedTextMessage(MasterSecret masterSecret, IncomingPushMessage message, PushMessageContent messageContent, - boolean secure, String groupId) + boolean secure) { EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context); + String groupId = messageContent.hasGroup() ? GroupUtil.getEncodedId(messageContent.getGroup().getId().toByteArray()) : null; IncomingTextMessage textMessage = new IncomingTextMessage(message, "", groupId); if (secure) { diff --git a/src/org/thoughtcrime/securesms/service/SmsReceiver.java b/src/org/thoughtcrime/securesms/service/SmsReceiver.java index aaaddb66b4..63eb31a6fa 100644 --- a/src/org/thoughtcrime/securesms/service/SmsReceiver.java +++ b/src/org/thoughtcrime/securesms/service/SmsReceiver.java @@ -32,6 +32,8 @@ import org.thoughtcrime.securesms.database.SmsDatabase; import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.protocol.WirePrefix; import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.recipients.RecipientFactory; +import org.thoughtcrime.securesms.recipients.RecipientFormattingException; import org.thoughtcrime.securesms.sms.IncomingEncryptedMessage; import org.thoughtcrime.securesms.sms.IncomingKeyExchangeMessage; import org.thoughtcrime.securesms.sms.IncomingPreKeyBundleMessage; @@ -46,6 +48,7 @@ import org.whispersystems.textsecure.crypto.MasterSecret; import org.whispersystems.textsecure.crypto.protocol.PreKeyWhisperMessage; import org.whispersystems.textsecure.crypto.protocol.WhisperMessageV2; import org.whispersystems.textsecure.storage.InvalidKeyIdException; +import org.whispersystems.textsecure.storage.RecipientDevice; import java.io.IOException; import java.util.List; @@ -80,8 +83,9 @@ public class SmsReceiver { if (masterSecret != null) { DecryptingQueue.scheduleDecryption(context, masterSecret, messageAndThreadId.first, messageAndThreadId.second, - message.getSender(), message.getMessageBody(), - message.isSecureMessage(), message.isKeyExchange()); + message.getSender(), message.getSenderDeviceId(), + message.getMessageBody(), message.isSecureMessage(), + message.isKeyExchange()); } return messageAndThreadId; @@ -106,8 +110,9 @@ public class SmsReceiver { Log.w("SmsReceiver", "Processing prekey message..."); try { - Recipient recipient = new Recipient(null, message.getSender(), null, null); - KeyExchangeProcessorV2 processor = new KeyExchangeProcessorV2(context, masterSecret, recipient); + Recipient recipient = RecipientFactory.getRecipientsFromString(context, message.getSender(), false).getPrimaryRecipient(); + RecipientDevice recipientDevice = new RecipientDevice(recipient.getRecipientId(), message.getSenderDeviceId()); + KeyExchangeProcessorV2 processor = new KeyExchangeProcessorV2(context, masterSecret, recipientDevice); SmsTransportDetails transportDetails = new SmsTransportDetails(); byte[] rawMessage = transportDetails.getDecodedMessage(message.getMessageBody().getBytes()); PreKeyWhisperMessage preKeyExchange = new PreKeyWhisperMessage(rawMessage); @@ -142,6 +147,9 @@ public class SmsReceiver { } catch (InvalidMessageException e) { Log.w("SmsReceiver", e); message.setCorrupted(true); + } catch (RecipientFormattingException e) { + Log.w("SmsReceiver", e); + message.setCorrupted(true); } return storeStandardMessage(masterSecret, message); @@ -152,9 +160,10 @@ public class SmsReceiver { { if (masterSecret != null && TextSecurePreferences.isAutoRespondKeyExchangeEnabled(context)) { try { - Recipient recipient = new Recipient(null, message.getSender(), null, null); + Recipient recipient = RecipientFactory.getRecipientsFromString(context, message.getSender(), false).getPrimaryRecipient(); + RecipientDevice recipientDevice = new RecipientDevice(recipient.getRecipientId(), message.getSenderDeviceId()); KeyExchangeMessage exchangeMessage = KeyExchangeMessage.createFor(message.getMessageBody()); - KeyExchangeProcessor processor = KeyExchangeProcessor.createFor(context, masterSecret, recipient, exchangeMessage); + KeyExchangeProcessor processor = KeyExchangeProcessor.createFor(context, masterSecret, recipientDevice, exchangeMessage); if (processor.isStale(exchangeMessage)) { message.setStale(true); @@ -175,6 +184,9 @@ public class SmsReceiver { } catch (InvalidMessageException e) { Log.w("SmsReceiver", e); message.setCorrupted(true); + } catch (RecipientFormattingException e) { + Log.w("SmsReceiver", e); + message.setCorrupted(true); } } diff --git a/src/org/thoughtcrime/securesms/service/SmsSender.java b/src/org/thoughtcrime/securesms/service/SmsSender.java index 175f87ef54..f1953564e8 100644 --- a/src/org/thoughtcrime/securesms/service/SmsSender.java +++ b/src/org/thoughtcrime/securesms/service/SmsSender.java @@ -46,11 +46,11 @@ public class SmsSender { } public void process(MasterSecret masterSecret, Intent intent) { - if (intent.getAction().equals(SendReceiveService.SEND_SMS_ACTION)) { + if (SendReceiveService.SEND_SMS_ACTION.equals(intent.getAction())) { handleSendMessage(masterSecret, intent); - } else if (intent.getAction().equals(SendReceiveService.SENT_SMS_ACTION)) { + } else if (SendReceiveService.SENT_SMS_ACTION.equals(intent.getAction())) { handleSentMessage(intent); - } else if (intent.getAction().equals(SendReceiveService.DELIVERED_SMS_ACTION)) { + } else if (SendReceiveService.DELIVERED_SMS_ACTION.equals(intent.getAction())) { handleDeliveredMessage(intent); } } diff --git a/src/org/thoughtcrime/securesms/sms/IncomingTextMessage.java b/src/org/thoughtcrime/securesms/sms/IncomingTextMessage.java index 3be14955f6..89027974f5 100644 --- a/src/org/thoughtcrime/securesms/sms/IncomingTextMessage.java +++ b/src/org/thoughtcrime/securesms/sms/IncomingTextMessage.java @@ -5,6 +5,7 @@ import android.os.Parcelable; import android.telephony.SmsMessage; import org.whispersystems.textsecure.push.IncomingPushMessage; +import org.whispersystems.textsecure.storage.RecipientDevice; import java.util.List; @@ -24,6 +25,7 @@ public class IncomingTextMessage implements Parcelable { private final String message; private final String sender; + private final int senderDeviceId; private final int protocol; private final String serviceCenterAddress; private final boolean replyPathPresent; @@ -34,6 +36,7 @@ public class IncomingTextMessage implements Parcelable { public IncomingTextMessage(SmsMessage message) { this.message = message.getDisplayMessageBody(); this.sender = message.getDisplayOriginatingAddress(); + this.senderDeviceId = RecipientDevice.DEFAULT_DEVICE_ID; this.protocol = message.getProtocolIdentifier(); this.serviceCenterAddress = message.getServiceCenterAddress(); this.replyPathPresent = message.isReplyPathPresent(); @@ -45,6 +48,7 @@ public class IncomingTextMessage implements Parcelable { public IncomingTextMessage(IncomingPushMessage message, String encodedBody, String groupId) { this.message = encodedBody; this.sender = message.getSource(); + this.senderDeviceId = message.getSourceDevice(); this.protocol = 31337; this.serviceCenterAddress = "GCM"; this.replyPathPresent = true; @@ -56,6 +60,7 @@ public class IncomingTextMessage implements Parcelable { public IncomingTextMessage(Parcel in) { this.message = in.readString(); this.sender = in.readString(); + this.senderDeviceId = in.readInt(); this.protocol = in.readInt(); this.serviceCenterAddress = in.readString(); this.replyPathPresent = (in.readInt() == 1); @@ -67,6 +72,7 @@ public class IncomingTextMessage implements Parcelable { public IncomingTextMessage(IncomingTextMessage base, String newBody) { this.message = newBody; this.sender = base.getSender(); + this.senderDeviceId = base.getSenderDeviceId(); this.protocol = base.getProtocol(); this.serviceCenterAddress = base.getServiceCenterAddress(); this.replyPathPresent = base.isReplyPathPresent(); @@ -84,6 +90,7 @@ public class IncomingTextMessage implements Parcelable { this.message = body.toString(); this.sender = fragments.get(0).getSender(); + this.senderDeviceId = fragments.get(0).getSenderDeviceId(); this.protocol = fragments.get(0).getProtocol(); this.serviceCenterAddress = fragments.get(0).getServiceCenterAddress(); this.replyPathPresent = fragments.get(0).isReplyPathPresent(); @@ -112,6 +119,10 @@ public class IncomingTextMessage implements Parcelable { return sender; } + public int getSenderDeviceId() { + return senderDeviceId; + } + public int getProtocol() { return protocol; } @@ -149,6 +160,7 @@ public class IncomingTextMessage implements Parcelable { public void writeToParcel(Parcel out, int flags) { out.writeString(message); out.writeString(sender); + out.writeInt(senderDeviceId); out.writeInt(protocol); out.writeString(serviceCenterAddress); out.writeInt(replyPathPresent ? 1 : 0); diff --git a/src/org/thoughtcrime/securesms/transport/MmsTransport.java b/src/org/thoughtcrime/securesms/transport/MmsTransport.java index d0b499949c..c7b5e0e610 100644 --- a/src/org/thoughtcrime/securesms/transport/MmsTransport.java +++ b/src/org/thoughtcrime/securesms/transport/MmsTransport.java @@ -29,9 +29,12 @@ import org.thoughtcrime.securesms.mms.MmsSendResult; import org.thoughtcrime.securesms.mms.TextTransport; import org.thoughtcrime.securesms.protocol.WirePrefix; import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.recipients.RecipientFactory; +import org.thoughtcrime.securesms.recipients.RecipientFormattingException; import org.whispersystems.textsecure.crypto.MasterSecret; import org.whispersystems.textsecure.crypto.SessionCipher; import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage; +import org.whispersystems.textsecure.storage.RecipientDevice; import org.whispersystems.textsecure.util.Hex; import java.io.IOException; @@ -153,12 +156,18 @@ public class MmsTransport { } private byte[] getEncryptedPdu(MasterSecret masterSecret, String recipientString, byte[] pduBytes) { - TextTransport transportDetails = new TextTransport(); - Recipient recipient = new Recipient(null, recipientString, null, null); - SessionCipher sessionCipher = SessionCipher.createFor(context, masterSecret, recipient); - CiphertextMessage ciphertextMessage = sessionCipher.encrypt(pduBytes); + try { + TextTransport transportDetails = new TextTransport(); + Recipient recipient = RecipientFactory.getRecipientsFromString(context, recipientString, false).getPrimaryRecipient(); + RecipientDevice recipientDevice = new RecipientDevice(recipient.getRecipientId(), RecipientDevice.DEFAULT_DEVICE_ID); + SessionCipher sessionCipher = SessionCipher.createFor(context, masterSecret, recipientDevice); + CiphertextMessage ciphertextMessage = sessionCipher.encrypt(pduBytes); - return transportDetails.getEncodedMessage(ciphertextMessage.serialize()); + return transportDetails.getEncodedMessage(ciphertextMessage.serialize()); + } catch (RecipientFormattingException e) { + Log.w("MmsTransport", e); + throw new AssertionError(e); + } } private boolean isInconsistentResponse(SendReq message, SendConf response) { diff --git a/src/org/thoughtcrime/securesms/transport/PushTransport.java b/src/org/thoughtcrime/securesms/transport/PushTransport.java index 1bf26195cc..7aef3939c6 100644 --- a/src/org/thoughtcrime/securesms/transport/PushTransport.java +++ b/src/org/thoughtcrime/securesms/transport/PushTransport.java @@ -23,6 +23,7 @@ import android.util.Log; import com.google.protobuf.ByteString; import org.thoughtcrime.securesms.crypto.KeyExchangeProcessorV2; +import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.model.SmsMessageRecord; import org.thoughtcrime.securesms.mms.PartParser; import org.thoughtcrime.securesms.push.PushServiceSocketFactory; @@ -30,22 +31,24 @@ import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientFactory; import org.thoughtcrime.securesms.recipients.RecipientFormattingException; import org.thoughtcrime.securesms.recipients.Recipients; -import org.thoughtcrime.securesms.util.TextSecurePreferences; +import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.Util; import org.whispersystems.textsecure.crypto.AttachmentCipher; import org.whispersystems.textsecure.crypto.InvalidKeyException; import org.whispersystems.textsecure.crypto.MasterSecret; import org.whispersystems.textsecure.crypto.SessionCipher; import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage; +import org.whispersystems.textsecure.push.MismatchedDevices; +import org.whispersystems.textsecure.push.MismatchedDevicesException; import org.whispersystems.textsecure.push.OutgoingPushMessage; +import org.whispersystems.textsecure.push.OutgoingPushMessageList; import org.whispersystems.textsecure.push.PreKeyEntity; +import org.whispersystems.textsecure.push.PushAddress; import org.whispersystems.textsecure.push.PushAttachmentData; import org.whispersystems.textsecure.push.PushAttachmentPointer; import org.whispersystems.textsecure.push.PushBody; -import org.whispersystems.textsecure.push.PushDestination; import org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent; import org.whispersystems.textsecure.push.PushServiceSocket; -import org.whispersystems.textsecure.push.RateLimitException; import org.whispersystems.textsecure.push.UnregisteredUserException; import org.whispersystems.textsecure.storage.SessionRecordV2; import org.whispersystems.textsecure.util.InvalidNumberException; @@ -70,79 +73,70 @@ public class PushTransport extends BaseTransport { public void deliver(SmsMessageRecord message) throws IOException { try { - String localNumber = TextSecurePreferences.getLocalNumber(context); - Recipient recipient = message.getIndividualRecipient(); - long threadId = message.getThreadId(); - PushServiceSocket socket = PushServiceSocketFactory.create(context); - PushDestination destination = PushDestination.create(context, localNumber, - recipient.getNumber()); + Recipient recipient = message.getIndividualRecipient(); + long threadId = message.getThreadId(); + PushServiceSocket socket = PushServiceSocketFactory.create(context); + byte[] plaintext = PushMessageContent.newBuilder() + .setBody(message.getBody().getBody()) + .build().toByteArray(); - String plaintextBody = message.getBody().getBody(); - byte[] plaintext = PushMessageContent.newBuilder().setBody(plaintextBody).build().toByteArray(); - PushBody pushBody = getEncryptedMessage(socket, threadId, recipient, destination, plaintext); - - socket.sendMessage(destination, pushBody); + deliver(socket, recipient, threadId, plaintext); context.sendBroadcast(constructSentIntent(context, message.getId(), message.getType(), true)); + } catch (UnregisteredUserException e) { Log.w("PushTransport", e); - destroySessions(e.getAddresses()); + //TODO We should probably remove the user from the directory? +// destroySessions(message.getIndividualRecipient()); throw new IOException("Not push registered after all."); - } catch (RateLimitException e) { - Log.w("PushTransport", e); - throw new IOException("Rate limit exceeded."); } catch (InvalidNumberException e) { Log.w("PushTransport", e); throw new IOException("Badly formatted number."); } } - public void deliver(SendReq message, List destinations, long threadId) - throws IOException - { + public void deliver(SendReq message, long threadId) throws IOException { + PushServiceSocket socket = PushServiceSocketFactory.create(context); + byte[] plaintext = getPlaintextMessage(socket, message); + String destination = message.getTo()[0].getString(); + + Recipients recipients; + try { - PushServiceSocket socket = PushServiceSocketFactory.create(context); - String messageBody = PartParser.getMessageText(message.getBody()); - List pushBodies = new LinkedList(); - - for (PushDestination destination : destinations) { - Recipients recipients = RecipientFactory.getRecipientsFromString(context, destination.getNumber(), false); - List attachments = getPushAttachmentPointers(socket, message.getBody()); - PushMessageContent.Builder builder = PushMessageContent.newBuilder(); - - if (messageBody != null) { - builder.setBody(messageBody); - } - - for (PushAttachmentPointer attachment : attachments) { - PushMessageContent.AttachmentPointer.Builder attachmentBuilder = - PushMessageContent.AttachmentPointer.newBuilder(); - - attachmentBuilder.setId(attachment.getId()); - attachmentBuilder.setContentType(attachment.getContentType()); - attachmentBuilder.setKey(ByteString.copyFrom(attachment.getKey())); - - builder.addAttachments(attachmentBuilder.build()); - } - - byte[] plaintext = builder.build().toByteArray(); - PushBody pushBody = getEncryptedMessage(socket, threadId, recipients.getPrimaryRecipient(), destination, plaintext); - - pushBodies.add(pushBody); + if (GroupUtil.isEncodedGroup(destination)) { + recipients = DatabaseFactory.getGroupDatabase(context) + .getGroupMembers(GroupUtil.getDecodedId(destination)); + } else { + recipients = RecipientFactory.getRecipientsFromString(context, destination, false); } - socket.sendMessage(destinations, pushBodies); - - } catch (UnregisteredUserException e) { - Log.w("PushTransport", e); - destroySessions(e.getAddresses()); - throw new IOException("No push registered after all."); - } catch (RateLimitException e) { - Log.w("PushTransport", e); - throw new IOException("Rate limit exceeded."); + for (Recipient recipient : recipients.getRecipientsList()) { + deliver(socket, recipient, threadId, plaintext); + } + } catch (UnregisteredUserException uue) { + // TODO: We should probably remove the user from the directory? + throw new IOException(uue); } catch (RecipientFormattingException e) { - Log.w("PushTransport", e); - throw new IOException("Bad destination!"); + throw new IOException(e); + } catch (InvalidNumberException e) { + throw new IOException(e); + } + } + + private void deliver(PushServiceSocket socket, Recipient recipient, long threadId, byte[] plaintext) + throws IOException, InvalidNumberException + { + for (int i=0;i<3;i++) { + try { + OutgoingPushMessageList messages = getEncryptedMessages(socket, threadId, + recipient, plaintext); + socket.sendMessage(messages); + + return; + } catch (MismatchedDevicesException mde) { + Log.w("PushTransport", mde); + handleMismatchedDevices(socket, threadId, recipient, mde.getMismatchedDevices()); + } } } @@ -170,23 +164,108 @@ public class PushTransport extends BaseTransport { return attachments; } - private PushBody getEncryptedMessage(PushServiceSocket socket, long threadId, Recipient recipient, - PushDestination pushDestination, byte[] plaintext) - throws IOException + private void handleMismatchedDevices(PushServiceSocket socket, long threadId, + Recipient recipient, + MismatchedDevices mismatchedDevices) + throws InvalidNumberException, IOException { - if (!SessionRecordV2.hasSession(context, masterSecret, recipient)) { - try { - PreKeyEntity preKey = socket.getPreKey(pushDestination); - KeyExchangeProcessorV2 processor = new KeyExchangeProcessorV2(context, masterSecret, recipient); + try { + String e164number = Util.canonicalizeNumber(context, recipient.getNumber()); + long recipientId = recipient.getRecipientId(); + + for (int extraDeviceId : mismatchedDevices.getExtraDevices()) { + PushAddress address = PushAddress.create(context, recipientId, e164number, extraDeviceId); + SessionRecordV2.delete(context, address); + } + + for (int missingDeviceId : mismatchedDevices.getMissingDevices()) { + PushAddress address = PushAddress.create(context, recipientId, e164number, missingDeviceId); + PreKeyEntity preKey = socket.getPreKey(address); + KeyExchangeProcessorV2 processor = new KeyExchangeProcessorV2(context, masterSecret, address); processor.processKeyExchangeMessage(preKey, threadId); + } + } catch (InvalidKeyException e) { + throw new IOException(e); + } + } + + private byte[] getPlaintextMessage(PushServiceSocket socket, SendReq message) throws IOException { + String messageBody = PartParser.getMessageText(message.getBody()); + List attachments = getPushAttachmentPointers(socket, message.getBody()); + + PushMessageContent.Builder builder = PushMessageContent.newBuilder(); + + if (GroupUtil.isEncodedGroup(message.getTo()[0].getString())) { + PushMessageContent.GroupContext.Builder groupBuilder = + PushMessageContent.GroupContext.newBuilder(); + + groupBuilder.setType(PushMessageContent.GroupContext.Type.DELIVER); + groupBuilder.setId(ByteString.copyFrom(GroupUtil.getDecodedId(message.getTo()[0].getString()))); + + builder.setGroup(groupBuilder.build()); + } + + if (messageBody != null) { + builder.setBody(messageBody); + } + + for (PushAttachmentPointer attachment : attachments) { + PushMessageContent.AttachmentPointer.Builder attachmentBuilder = + PushMessageContent.AttachmentPointer.newBuilder(); + + attachmentBuilder.setId(attachment.getId()); + attachmentBuilder.setContentType(attachment.getContentType()); + attachmentBuilder.setKey(ByteString.copyFrom(attachment.getKey())); + + builder.addAttachments(attachmentBuilder.build()); + } + + return builder.build().toByteArray(); + } + + private OutgoingPushMessageList getEncryptedMessages(PushServiceSocket socket, long threadId, + Recipient recipient, byte[] plaintext) + throws IOException, InvalidNumberException + { + String e164number = Util.canonicalizeNumber(context, recipient.getNumber()); + long recipientId = recipient.getRecipientId(); + PushAddress masterDevice = PushAddress.create(context, recipientId, e164number, 1); + PushBody masterBody = getEncryptedMessage(socket, threadId, masterDevice, plaintext); + + List messages = new LinkedList(); + messages.add(new OutgoingPushMessage(masterDevice, masterBody)); + + for (int deviceId : SessionRecordV2.getSessionSubDevices(context, recipient)) { + PushAddress device = PushAddress.create(context, recipientId, e164number, deviceId); + PushBody body = getEncryptedMessage(socket, threadId, device, plaintext); + + messages.add(new OutgoingPushMessage(device, body)); + } + + return new OutgoingPushMessageList(e164number, masterDevice.getRelay(), messages); + } + + private PushBody getEncryptedMessage(PushServiceSocket socket, long threadId, + PushAddress pushAddress, byte[] plaintext) + throws IOException + { + if (!SessionRecordV2.hasSession(context, masterSecret, pushAddress)) { + try { + List preKeys = socket.getPreKeys(pushAddress); + + for (PreKeyEntity preKey : preKeys) { + PushAddress device = PushAddress.create(context, pushAddress.getRecipientId(), pushAddress.getNumber(), preKey.getDeviceId()); + KeyExchangeProcessorV2 processor = new KeyExchangeProcessorV2(context, masterSecret, device); + + processor.processKeyExchangeMessage(preKey, threadId); + } } catch (InvalidKeyException e) { - Log.w("PushTransport", e); - throw new IOException("Invalid PreKey!"); + throw new IOException(e); } } - SessionCipher cipher = SessionCipher.createFor(context, masterSecret, recipient); + SessionCipher cipher = SessionCipher.createFor(context, masterSecret, pushAddress); CiphertextMessage message = cipher.encrypt(plaintext); if (message.getType() == CiphertextMessage.PREKEY_WHISPER_TYPE) { @@ -198,15 +277,7 @@ public class PushTransport extends BaseTransport { } } - private void destroySessions(List unregisteredUsers) { - for (String unregisteredUser : unregisteredUsers) { - Log.w("PushTransport", "Destroying session for: " + unregisteredUser); - try { - Recipients recipients = RecipientFactory.getRecipientsFromString(context, unregisteredUser, false); - SessionRecordV2.delete(context, recipients.getPrimaryRecipient()); - } catch (RecipientFormattingException e) { - Log.w("PushTransport", e); - } - } + private void destroySessions(Recipient recipient) { + SessionRecordV2.deleteAll(context, recipient); } } diff --git a/src/org/thoughtcrime/securesms/transport/SmsTransport.java b/src/org/thoughtcrime/securesms/transport/SmsTransport.java index 8959fd234d..2ed909a5a0 100644 --- a/src/org/thoughtcrime/securesms/transport/SmsTransport.java +++ b/src/org/thoughtcrime/securesms/transport/SmsTransport.java @@ -34,6 +34,7 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.textsecure.crypto.MasterSecret; import org.whispersystems.textsecure.crypto.SessionCipher; import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage; +import org.whispersystems.textsecure.storage.RecipientDevice; import java.util.ArrayList; @@ -160,9 +161,10 @@ public class SmsTransport extends BaseTransport { OutgoingTextMessage message) { Recipient recipient = message.getRecipients().getPrimaryRecipient(); + RecipientDevice recipientDevice = new RecipientDevice(recipient.getRecipientId(), RecipientDevice.DEFAULT_DEVICE_ID); String body = message.getMessageBody(); SmsTransportDetails transportDetails = new SmsTransportDetails(); - SessionCipher sessionCipher = SessionCipher.createFor(context, masterSecret, recipient); + SessionCipher sessionCipher = SessionCipher.createFor(context, masterSecret, recipientDevice); byte[] paddedPlaintext = transportDetails.getPaddedMessageBody(body.getBytes()); CiphertextMessage ciphertextMessage = sessionCipher.encrypt(paddedPlaintext); String encodedCiphertext = new String(transportDetails.getEncodedMessage(ciphertextMessage.serialize())); diff --git a/src/org/thoughtcrime/securesms/transport/UniversalTransport.java b/src/org/thoughtcrime/securesms/transport/UniversalTransport.java index 923517141d..5da614301f 100644 --- a/src/org/thoughtcrime/securesms/transport/UniversalTransport.java +++ b/src/org/thoughtcrime/securesms/transport/UniversalTransport.java @@ -23,21 +23,18 @@ import org.thoughtcrime.securesms.database.model.SmsMessageRecord; import org.thoughtcrime.securesms.mms.MmsSendResult; import org.thoughtcrime.securesms.push.PushServiceSocketFactory; import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; import org.whispersystems.textsecure.crypto.MasterSecret; import org.whispersystems.textsecure.directory.Directory; import org.whispersystems.textsecure.directory.NotInDirectoryException; import org.whispersystems.textsecure.push.ContactTokenDetails; -import org.whispersystems.textsecure.push.PushDestination; import org.whispersystems.textsecure.push.PushServiceSocket; import org.whispersystems.textsecure.util.InvalidNumberException; import java.io.IOException; -import java.util.LinkedList; -import java.util.List; -import ws.com.google.android.mms.pdu.EncodedStringValue; import ws.com.google.android.mms.pdu.SendReq; public class UniversalTransport { @@ -85,60 +82,56 @@ public class UniversalTransport { public MmsSendResult deliver(SendReq mediaMessage, long threadId) throws UndeliverableMessageException { + if (Util.isEmpty(mediaMessage.getTo())) { + throw new UndeliverableMessageException("No destination specified"); + } + if (!TextSecurePreferences.isPushRegistered(context)) { return mmsTransport.deliver(mediaMessage); } - try { - List destinations = getMediaDestinations(mediaMessage); + if (isMultipleRecipients(mediaMessage)) { + return mmsTransport.deliver(mediaMessage); + } - if (isPushTransport(destinations)) { - try { - Log.w("UniversalTransport", "Delivering media message with GCM..."); - pushTransport.deliver(mediaMessage, destinations, threadId); - return new MmsSendResult("push".getBytes("UTF-8"), 0, true); - } catch (IOException ioe) { - Log.w("UniversalTransport", ioe); - return mmsTransport.deliver(mediaMessage); - } - } else { - Log.w("UniversalTransport", "Delivering media message with MMS..."); + if (isPushTransport(mediaMessage.getTo()[0].getString())) { + try { + Log.w("UniversalTransport", "Delivering media message with GCM..."); + pushTransport.deliver(mediaMessage, threadId); + return new MmsSendResult("push".getBytes("UTF-8"), 0, true); + } catch (IOException ioe) { + Log.w("UniversalTransport", ioe); return mmsTransport.deliver(mediaMessage); } - } catch (InvalidNumberException e) { - Log.w("UniversalTransport", e); + } else { + Log.w("UniversalTransport", "Delivering media message with MMS..."); return mmsTransport.deliver(mediaMessage); } } - private List getMediaDestinations(SendReq mediaMessage) - throws InvalidNumberException - { - String localNumber = TextSecurePreferences.getLocalNumber(context); - LinkedList destinations = new LinkedList(); + public boolean isMultipleRecipients(SendReq mediaMessage) { + int recipientCount = 0; if (mediaMessage.getTo() != null) { - for (EncodedStringValue to : mediaMessage.getTo()) { - destinations.add(PushDestination.create(context, localNumber, to.getString())); - } + recipientCount += mediaMessage.getTo().length; } if (mediaMessage.getCc() != null) { - for (EncodedStringValue cc : mediaMessage.getCc()) { - destinations.add(PushDestination.create(context, localNumber, cc.getString())); - } + recipientCount += mediaMessage.getCc().length; } if (mediaMessage.getBcc() != null) { - for (EncodedStringValue bcc : mediaMessage.getBcc()) { - destinations.add(PushDestination.create(context, localNumber, bcc.getString())); - } + recipientCount += mediaMessage.getBcc().length; } - return destinations; + return recipientCount > 1; } private boolean isPushTransport(String destination) { + if (GroupUtil.isEncodedGroup(destination)) { + return true; + } + Directory directory = Directory.getInstance(context); try { @@ -163,14 +156,4 @@ public class UniversalTransport { } } } - - private boolean isPushTransport(List destinations) { - for (PushDestination destination : destinations) { - if (!isPushTransport(destination.getNumber())) { - return false; - } - } - - return true; - } } diff --git a/src/org/thoughtcrime/securesms/util/GroupUtil.java b/src/org/thoughtcrime/securesms/util/GroupUtil.java new file mode 100644 index 0000000000..843641c3a7 --- /dev/null +++ b/src/org/thoughtcrime/securesms/util/GroupUtil.java @@ -0,0 +1,27 @@ +package org.thoughtcrime.securesms.util; + +import org.whispersystems.textsecure.util.Hex; + +import java.io.IOException; + +public class GroupUtil { + + private static final String ENCODED_GROUP_PREFIX = "__textsecure_group__!"; + + public static String getEncodedId(byte[] groupId) { + return ENCODED_GROUP_PREFIX + Hex.toStringCondensed(groupId); + } + + public static byte[] getDecodedId(String groupId) throws IOException { + if (!isEncodedGroup(groupId)) { + throw new IOException("Invalid encoding"); + } + + return Hex.fromStringCondensed(groupId.split("!", 2)[1]); + } + + public static boolean isEncodedGroup(String groupId) { + return groupId.startsWith(ENCODED_GROUP_PREFIX); + } + +} diff --git a/src/org/thoughtcrime/securesms/util/NumberUtil.java b/src/org/thoughtcrime/securesms/util/NumberUtil.java index 84749ad9e7..5cb3433451 100644 --- a/src/org/thoughtcrime/securesms/util/NumberUtil.java +++ b/src/org/thoughtcrime/securesms/util/NumberUtil.java @@ -34,6 +34,12 @@ public class NumberUtil { return PhoneNumberUtils.isWellFormedSmsAddress(number) || isValidEmail(number); } + public static boolean isValidSmsOrEmailOrGroup(String number) { + return PhoneNumberUtils.isWellFormedSmsAddress(number) || + isValidEmail(number) || + GroupUtil.isEncodedGroup(number); + } + public static String filterNumber(String number) { if (number == null) return null;