Support for multi-device.

1) In addition to the Recipient interface, there is now
   RecipientDevice.  A Recipient can have multiple corresponding
   RecipientDevices.  All addressing is done to a Recipient, but
   crypto sessions and transport delivery are done to
   RecipientDevice.

2) The Push transport handles the discovery and session setup
   of additional Recipient devices.

3) Some internal rejiggering of Groups.
This commit is contained in:
Moxie Marlinspike 2014-02-02 19:38:06 -08:00
parent 49daa45dca
commit 0ace469d74
70 changed files with 1118 additions and 668 deletions

View File

@ -3,6 +3,6 @@
package="org.whispersystems.textsecure" package="org.whispersystems.textsecure"
android:versionCode="1" android:versionCode="1"
android:versionName="0.1"> android:versionName="0.1">
<uses-sdk android:minSdkVersion="8" android:targetSdkVersion="16"/> <uses-sdk android:minSdkVersion="9" android:targetSdkVersion="16"/>
<application /> <application />
</manifest> </manifest>

View File

@ -4,7 +4,7 @@ buildscript {
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:0.6.1' classpath 'com.android.tools.build:gradle:0.7.+'
} }
} }

View File

@ -6,6 +6,7 @@ option java_outer_classname = "PushMessageProtos";
message IncomingPushMessageSignal { message IncomingPushMessageSignal {
optional uint32 type = 1; optional uint32 type = 1;
optional string source = 2; optional string source = 2;
optional uint32 sourceDevice = 7;
optional string relay = 3; optional string relay = 3;
// repeated string destinations = 4; // No longer supported // repeated string destinations = 4; // No longer supported
optional uint64 timestamp = 5; optional uint64 timestamp = 5;

View File

@ -20,7 +20,7 @@ package org.whispersystems.textsecure.crypto;
import android.content.Context; import android.content.Context;
import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage; 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.SessionRecordV1;
import org.whispersystems.textsecure.storage.SessionRecordV2; import org.whispersystems.textsecure.storage.SessionRecordV2;
@ -31,13 +31,14 @@ public abstract class SessionCipher {
public abstract CiphertextMessage encrypt(byte[] paddedMessage); public abstract CiphertextMessage encrypt(byte[] paddedMessage);
public abstract byte[] decrypt(byte[] decodedMessage) throws InvalidMessageException; public abstract byte[] decrypt(byte[] decodedMessage) throws InvalidMessageException;
public static SessionCipher createFor(Context context, MasterSecret masterSecret, public static SessionCipher createFor(Context context,
CanonicalRecipientAddress recipient) MasterSecret masterSecret,
RecipientDevice recipient)
{ {
if (SessionRecordV2.hasSession(context, masterSecret, recipient)) { if (SessionRecordV2.hasSession(context, masterSecret, recipient)) {
return new SessionCipherV2(context, masterSecret, recipient); return new SessionCipherV2(context, masterSecret, recipient);
} else if (SessionRecordV1.hasSession(context, recipient)) { } else if (SessionRecordV1.hasSession(context, recipient.getRecipientId())) {
return new SessionCipherV1(context, masterSecret, recipient); return new SessionCipherV1(context, masterSecret, recipient.getRecipient());
} else { } else {
throw new AssertionError("Attempt to initialize cipher for non-existing session."); throw new AssertionError("Attempt to initialize cipher for non-existing session.");
} }

View File

@ -9,7 +9,8 @@ import org.whispersystems.textsecure.crypto.kdf.DerivedSecrets;
import org.whispersystems.textsecure.crypto.kdf.NKDF; import org.whispersystems.textsecure.crypto.kdf.NKDF;
import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage; import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage;
import org.whispersystems.textsecure.crypto.protocol.WhisperMessageV1; 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.InvalidKeyIdException;
import org.whispersystems.textsecure.storage.LocalKeyRecord; import org.whispersystems.textsecure.storage.LocalKeyRecord;
import org.whispersystems.textsecure.storage.RemoteKeyRecord; import org.whispersystems.textsecure.storage.RemoteKeyRecord;
@ -31,10 +32,11 @@ public class SessionCipherV1 extends SessionCipher {
private final Context context; private final Context context;
private final MasterSecret masterSecret; private final MasterSecret masterSecret;
private final CanonicalRecipientAddress recipient; private final CanonicalRecipient recipient;
public SessionCipherV1(Context context, MasterSecret masterSecret, public SessionCipherV1(Context context,
CanonicalRecipientAddress recipient) MasterSecret masterSecret,
CanonicalRecipient recipient)
{ {
this.context = context; this.context = context;
this.masterSecret = masterSecret; this.masterSecret = masterSecret;
@ -219,7 +221,7 @@ public class SessionCipherV1 extends SessionCipher {
} }
private KeyRecords getKeyRecords(Context context, MasterSecret masterSecret, private KeyRecords getKeyRecords(Context context, MasterSecret masterSecret,
CanonicalRecipientAddress recipient) CanonicalRecipient recipient)
{ {
LocalKeyRecord localKeyRecord = new LocalKeyRecord(context, masterSecret, recipient); LocalKeyRecord localKeyRecord = new LocalKeyRecord(context, masterSecret, recipient);
RemoteKeyRecord remoteKeyRecord = new RemoteKeyRecord(context, recipient); RemoteKeyRecord remoteKeyRecord = new RemoteKeyRecord(context, recipient);

View File

@ -1,7 +1,6 @@
package org.whispersystems.textsecure.crypto; package org.whispersystems.textsecure.crypto;
import android.content.Context; import android.content.Context;
import android.util.Log;
import android.util.Pair; import android.util.Pair;
import org.whispersystems.textsecure.crypto.ecc.Curve; 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.ChainKey;
import org.whispersystems.textsecure.crypto.ratchet.MessageKeys; import org.whispersystems.textsecure.crypto.ratchet.MessageKeys;
import org.whispersystems.textsecure.crypto.ratchet.RootKey; 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.storage.SessionRecordV2;
import org.whispersystems.textsecure.util.Conversions; import org.whispersystems.textsecure.util.Conversions;
@ -31,11 +30,11 @@ public class SessionCipherV2 extends SessionCipher {
private final Context context; private final Context context;
private final MasterSecret masterSecret; private final MasterSecret masterSecret;
private final CanonicalRecipientAddress recipient; private final RecipientDevice recipient;
public SessionCipherV2(Context context, public SessionCipherV2(Context context,
MasterSecret masterSecret, MasterSecret masterSecret,
CanonicalRecipientAddress recipient) RecipientDevice recipient)
{ {
this.context = context; this.context = context;
this.masterSecret = masterSecret; this.masterSecret = masterSecret;

View File

@ -37,6 +37,7 @@ public class IncomingPushMessage implements PushMessage, Parcelable {
private int type; private int type;
private String source; private String source;
private int sourceDevice;
private byte[] message; private byte[] message;
private long timestamp; private long timestamp;
private String relay; private String relay;
@ -44,6 +45,7 @@ public class IncomingPushMessage implements PushMessage, Parcelable {
private IncomingPushMessage(IncomingPushMessage message, byte[] body) { private IncomingPushMessage(IncomingPushMessage message, byte[] body) {
this.type = message.type; this.type = message.type;
this.source = message.source; this.source = message.source;
this.sourceDevice = message.sourceDevice;
this.timestamp = message.timestamp; this.timestamp = message.timestamp;
this.relay = message.relay; this.relay = message.relay;
this.message = body; this.message = body;
@ -52,6 +54,7 @@ public class IncomingPushMessage implements PushMessage, Parcelable {
public IncomingPushMessage(IncomingPushMessageSignal signal) { public IncomingPushMessage(IncomingPushMessageSignal signal) {
this.type = signal.getType(); this.type = signal.getType();
this.source = signal.getSource(); this.source = signal.getSource();
this.sourceDevice = signal.getSourceDevice();
this.message = signal.getMessage().toByteArray(); this.message = signal.getMessage().toByteArray();
this.timestamp = signal.getTimestamp(); this.timestamp = signal.getTimestamp();
this.relay = signal.getRelay(); this.relay = signal.getRelay();
@ -60,6 +63,7 @@ public class IncomingPushMessage implements PushMessage, Parcelable {
public IncomingPushMessage(Parcel in) { public IncomingPushMessage(Parcel in) {
this.type = in.readInt(); this.type = in.readInt();
this.source = in.readString(); this.source = in.readString();
this.sourceDevice = in.readInt();
if (in.readInt() == 1) { if (in.readInt() == 1) {
this.relay = in.readString(); this.relay = in.readString();
@ -70,11 +74,12 @@ public class IncomingPushMessage implements PushMessage, Parcelable {
this.timestamp = in.readLong(); this.timestamp = in.readLong();
} }
public IncomingPushMessage(int type, String source, public IncomingPushMessage(int type, String source, int sourceDevice,
byte[] body, long timestamp) byte[] body, long timestamp)
{ {
this.type = type; this.type = type;
this.source = source; this.source = source;
this.sourceDevice = sourceDevice;
this.message = body; this.message = body;
this.timestamp = timestamp; this.timestamp = timestamp;
} }
@ -91,6 +96,10 @@ public class IncomingPushMessage implements PushMessage, Parcelable {
return source; return source;
} }
public int getSourceDevice() {
return sourceDevice;
}
public byte[] getBody() { public byte[] getBody() {
return message; return message;
} }
@ -104,6 +113,7 @@ public class IncomingPushMessage implements PushMessage, Parcelable {
public void writeToParcel(Parcel dest, int flags) { public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(type); dest.writeInt(type);
dest.writeString(source); dest.writeString(source);
dest.writeInt(sourceDevice);
dest.writeInt(relay == null ? 0 : 1); dest.writeInt(relay == null ? 0 : 1);
if (relay != null) { if (relay != null) {
dest.writeString(relay); dest.writeString(relay);

View File

@ -0,0 +1,17 @@
package org.whispersystems.textsecure.push;
import java.util.List;
public class MismatchedDevices {
private List<Integer> missingDevices;
private List<Integer> extraDevices;
public List<Integer> getMissingDevices() {
return missingDevices;
}
public List<Integer> getExtraDevices() {
return extraDevices;
}
}

View File

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

View File

@ -21,23 +21,17 @@ import org.whispersystems.textsecure.util.Base64;
public class OutgoingPushMessage implements PushMessage { public class OutgoingPushMessage implements PushMessage {
private int type; private int type;
private String destination; private int destinationDeviceId;
private String body; private String body;
private String relay;
public OutgoingPushMessage(String destination, byte[] body, int type) { public OutgoingPushMessage(PushAddress address, PushBody body) {
this(null, destination, body, type); this.type = body.getType();
this.destinationDeviceId = address.getDeviceId();
this.body = Base64.encodeBytes(body.getBody());
} }
public OutgoingPushMessage(String relay, String destination, byte[] body, int type) { public int getDestinationDeviceId() {
this.relay = relay; return destinationDeviceId;
this.destination = destination;
this.body = Base64.encodeBytes(body);
this.type = type;
}
public String getDestination() {
return destination;
} }
public String getBody() { public String getBody() {
@ -47,8 +41,4 @@ public class OutgoingPushMessage implements PushMessage {
public int getType() { public int getType() {
return type; return type;
} }
public String getRelay() {
return relay;
}
} }

View File

@ -5,18 +5,27 @@ import java.util.List;
public class OutgoingPushMessageList { public class OutgoingPushMessageList {
private String destination;
private String relay;
private List<OutgoingPushMessage> messages; private List<OutgoingPushMessage> messages;
public OutgoingPushMessageList(OutgoingPushMessage message) { public OutgoingPushMessageList(String destination, String relay, List<OutgoingPushMessage> messages) {
this.messages = new LinkedList<OutgoingPushMessage>(); this.destination = destination;
this.messages.add(message); this.relay = relay;
this.messages = messages;
} }
public OutgoingPushMessageList(List<OutgoingPushMessage> messages) { public String getDestination() {
this.messages = messages; return destination;
} }
public List<OutgoingPushMessage> getMessages() { public List<OutgoingPushMessage> getMessages() {
return messages; return messages;
} }
public String getRelay() {
return relay;
}
} }

View File

@ -8,6 +8,7 @@ import com.google.thoughtcrimegson.JsonParseException;
import com.google.thoughtcrimegson.JsonPrimitive; import com.google.thoughtcrimegson.JsonPrimitive;
import com.google.thoughtcrimegson.JsonSerializationContext; import com.google.thoughtcrimegson.JsonSerializationContext;
import com.google.thoughtcrimegson.JsonSerializer; import com.google.thoughtcrimegson.JsonSerializer;
import com.google.thoughtcrimegson.annotations.Expose;
import org.whispersystems.textsecure.crypto.IdentityKey; import org.whispersystems.textsecure.crypto.IdentityKey;
import org.whispersystems.textsecure.crypto.InvalidKeyException; import org.whispersystems.textsecure.crypto.InvalidKeyException;
@ -20,6 +21,9 @@ import java.lang.reflect.Type;
public class PreKeyEntity { public class PreKeyEntity {
@Expose(serialize = false)
private int deviceId;
private int keyId; private int keyId;
private ECPublicKey publicKey; private ECPublicKey publicKey;
private IdentityKey identityKey; private IdentityKey identityKey;
@ -30,6 +34,10 @@ public class PreKeyEntity {
this.identityKey = identityKey; this.identityKey = identityKey;
} }
public int getDeviceId() {
return deviceId;
}
public int getKeyId() { public int getKeyId() {
return keyId; return keyId;
} }

View File

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

View File

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

View File

@ -19,6 +19,10 @@ public final class PushMessageProtos {
boolean hasSource(); boolean hasSource();
String getSource(); String getSource();
// optional uint32 sourceDevice = 7;
boolean hasSourceDevice();
int getSourceDevice();
// optional string relay = 3; // optional string relay = 3;
boolean hasRelay(); boolean hasRelay();
String getRelay(); 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; // optional string relay = 3;
public static final int RELAY_FIELD_NUMBER = 3; public static final int RELAY_FIELD_NUMBER = 3;
private java.lang.Object relay_; private java.lang.Object relay_;
public boolean hasRelay() { public boolean hasRelay() {
return ((bitField0_ & 0x00000004) == 0x00000004); return ((bitField0_ & 0x00000008) == 0x00000008);
} }
public String getRelay() { public String getRelay() {
java.lang.Object ref = relay_; java.lang.Object ref = relay_;
@ -138,7 +152,7 @@ public final class PushMessageProtos {
public static final int TIMESTAMP_FIELD_NUMBER = 5; public static final int TIMESTAMP_FIELD_NUMBER = 5;
private long timestamp_; private long timestamp_;
public boolean hasTimestamp() { public boolean hasTimestamp() {
return ((bitField0_ & 0x00000008) == 0x00000008); return ((bitField0_ & 0x00000010) == 0x00000010);
} }
public long getTimestamp() { public long getTimestamp() {
return timestamp_; return timestamp_;
@ -148,7 +162,7 @@ public final class PushMessageProtos {
public static final int MESSAGE_FIELD_NUMBER = 6; public static final int MESSAGE_FIELD_NUMBER = 6;
private com.google.protobuf.ByteString message_; private com.google.protobuf.ByteString message_;
public boolean hasMessage() { public boolean hasMessage() {
return ((bitField0_ & 0x00000010) == 0x00000010); return ((bitField0_ & 0x00000020) == 0x00000020);
} }
public com.google.protobuf.ByteString getMessage() { public com.google.protobuf.ByteString getMessage() {
return message_; return message_;
@ -157,6 +171,7 @@ public final class PushMessageProtos {
private void initFields() { private void initFields() {
type_ = 0; type_ = 0;
source_ = ""; source_ = "";
sourceDevice_ = 0;
relay_ = ""; relay_ = "";
timestamp_ = 0L; timestamp_ = 0L;
message_ = com.google.protobuf.ByteString.EMPTY; message_ = com.google.protobuf.ByteString.EMPTY;
@ -179,15 +194,18 @@ public final class PushMessageProtos {
if (((bitField0_ & 0x00000002) == 0x00000002)) { if (((bitField0_ & 0x00000002) == 0x00000002)) {
output.writeBytes(2, getSourceBytes()); output.writeBytes(2, getSourceBytes());
} }
if (((bitField0_ & 0x00000004) == 0x00000004)) { if (((bitField0_ & 0x00000008) == 0x00000008)) {
output.writeBytes(3, getRelayBytes()); output.writeBytes(3, getRelayBytes());
} }
if (((bitField0_ & 0x00000008) == 0x00000008)) { if (((bitField0_ & 0x00000010) == 0x00000010)) {
output.writeUInt64(5, timestamp_); output.writeUInt64(5, timestamp_);
} }
if (((bitField0_ & 0x00000010) == 0x00000010)) { if (((bitField0_ & 0x00000020) == 0x00000020)) {
output.writeBytes(6, message_); output.writeBytes(6, message_);
} }
if (((bitField0_ & 0x00000004) == 0x00000004)) {
output.writeUInt32(7, sourceDevice_);
}
getUnknownFields().writeTo(output); getUnknownFields().writeTo(output);
} }
@ -205,18 +223,22 @@ public final class PushMessageProtos {
size += com.google.protobuf.CodedOutputStream size += com.google.protobuf.CodedOutputStream
.computeBytesSize(2, getSourceBytes()); .computeBytesSize(2, getSourceBytes());
} }
if (((bitField0_ & 0x00000004) == 0x00000004)) { if (((bitField0_ & 0x00000008) == 0x00000008)) {
size += com.google.protobuf.CodedOutputStream size += com.google.protobuf.CodedOutputStream
.computeBytesSize(3, getRelayBytes()); .computeBytesSize(3, getRelayBytes());
} }
if (((bitField0_ & 0x00000008) == 0x00000008)) { if (((bitField0_ & 0x00000010) == 0x00000010)) {
size += com.google.protobuf.CodedOutputStream size += com.google.protobuf.CodedOutputStream
.computeUInt64Size(5, timestamp_); .computeUInt64Size(5, timestamp_);
} }
if (((bitField0_ & 0x00000010) == 0x00000010)) { if (((bitField0_ & 0x00000020) == 0x00000020)) {
size += com.google.protobuf.CodedOutputStream size += com.google.protobuf.CodedOutputStream
.computeBytesSize(6, message_); .computeBytesSize(6, message_);
} }
if (((bitField0_ & 0x00000004) == 0x00000004)) {
size += com.google.protobuf.CodedOutputStream
.computeUInt32Size(7, sourceDevice_);
}
size += getUnknownFields().getSerializedSize(); size += getUnknownFields().getSerializedSize();
memoizedSerializedSize = size; memoizedSerializedSize = size;
return size; return size;
@ -345,12 +367,14 @@ public final class PushMessageProtos {
bitField0_ = (bitField0_ & ~0x00000001); bitField0_ = (bitField0_ & ~0x00000001);
source_ = ""; source_ = "";
bitField0_ = (bitField0_ & ~0x00000002); bitField0_ = (bitField0_ & ~0x00000002);
relay_ = ""; sourceDevice_ = 0;
bitField0_ = (bitField0_ & ~0x00000004); bitField0_ = (bitField0_ & ~0x00000004);
timestamp_ = 0L; relay_ = "";
bitField0_ = (bitField0_ & ~0x00000008); bitField0_ = (bitField0_ & ~0x00000008);
message_ = com.google.protobuf.ByteString.EMPTY; timestamp_ = 0L;
bitField0_ = (bitField0_ & ~0x00000010); bitField0_ = (bitField0_ & ~0x00000010);
message_ = com.google.protobuf.ByteString.EMPTY;
bitField0_ = (bitField0_ & ~0x00000020);
return this; return this;
} }
@ -400,14 +424,18 @@ public final class PushMessageProtos {
if (((from_bitField0_ & 0x00000004) == 0x00000004)) { if (((from_bitField0_ & 0x00000004) == 0x00000004)) {
to_bitField0_ |= 0x00000004; to_bitField0_ |= 0x00000004;
} }
result.relay_ = relay_; result.sourceDevice_ = sourceDevice_;
if (((from_bitField0_ & 0x00000008) == 0x00000008)) { if (((from_bitField0_ & 0x00000008) == 0x00000008)) {
to_bitField0_ |= 0x00000008; to_bitField0_ |= 0x00000008;
} }
result.timestamp_ = timestamp_; result.relay_ = relay_;
if (((from_bitField0_ & 0x00000010) == 0x00000010)) { if (((from_bitField0_ & 0x00000010) == 0x00000010)) {
to_bitField0_ |= 0x00000010; to_bitField0_ |= 0x00000010;
} }
result.timestamp_ = timestamp_;
if (((from_bitField0_ & 0x00000020) == 0x00000020)) {
to_bitField0_ |= 0x00000020;
}
result.message_ = message_; result.message_ = message_;
result.bitField0_ = to_bitField0_; result.bitField0_ = to_bitField0_;
onBuilt(); onBuilt();
@ -431,6 +459,9 @@ public final class PushMessageProtos {
if (other.hasSource()) { if (other.hasSource()) {
setSource(other.getSource()); setSource(other.getSource());
} }
if (other.hasSourceDevice()) {
setSourceDevice(other.getSourceDevice());
}
if (other.hasRelay()) { if (other.hasRelay()) {
setRelay(other.getRelay()); setRelay(other.getRelay());
} }
@ -482,20 +513,25 @@ public final class PushMessageProtos {
break; break;
} }
case 26: { case 26: {
bitField0_ |= 0x00000004; bitField0_ |= 0x00000008;
relay_ = input.readBytes(); relay_ = input.readBytes();
break; break;
} }
case 40: { case 40: {
bitField0_ |= 0x00000008; bitField0_ |= 0x00000010;
timestamp_ = input.readUInt64(); timestamp_ = input.readUInt64();
break; break;
} }
case 50: { case 50: {
bitField0_ |= 0x00000010; bitField0_ |= 0x00000020;
message_ = input.readBytes(); message_ = input.readBytes();
break; break;
} }
case 56: {
bitField0_ |= 0x00000004;
sourceDevice_ = input.readUInt32();
break;
}
} }
} }
} }
@ -559,10 +595,31 @@ public final class PushMessageProtos {
onChanged(); 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; // optional string relay = 3;
private java.lang.Object relay_ = ""; private java.lang.Object relay_ = "";
public boolean hasRelay() { public boolean hasRelay() {
return ((bitField0_ & 0x00000004) == 0x00000004); return ((bitField0_ & 0x00000008) == 0x00000008);
} }
public String getRelay() { public String getRelay() {
java.lang.Object ref = relay_; java.lang.Object ref = relay_;
@ -578,19 +635,19 @@ public final class PushMessageProtos {
if (value == null) { if (value == null) {
throw new NullPointerException(); throw new NullPointerException();
} }
bitField0_ |= 0x00000004; bitField0_ |= 0x00000008;
relay_ = value; relay_ = value;
onChanged(); onChanged();
return this; return this;
} }
public Builder clearRelay() { public Builder clearRelay() {
bitField0_ = (bitField0_ & ~0x00000004); bitField0_ = (bitField0_ & ~0x00000008);
relay_ = getDefaultInstance().getRelay(); relay_ = getDefaultInstance().getRelay();
onChanged(); onChanged();
return this; return this;
} }
void setRelay(com.google.protobuf.ByteString value) { void setRelay(com.google.protobuf.ByteString value) {
bitField0_ |= 0x00000004; bitField0_ |= 0x00000008;
relay_ = value; relay_ = value;
onChanged(); onChanged();
} }
@ -598,19 +655,19 @@ public final class PushMessageProtos {
// optional uint64 timestamp = 5; // optional uint64 timestamp = 5;
private long timestamp_ ; private long timestamp_ ;
public boolean hasTimestamp() { public boolean hasTimestamp() {
return ((bitField0_ & 0x00000008) == 0x00000008); return ((bitField0_ & 0x00000010) == 0x00000010);
} }
public long getTimestamp() { public long getTimestamp() {
return timestamp_; return timestamp_;
} }
public Builder setTimestamp(long value) { public Builder setTimestamp(long value) {
bitField0_ |= 0x00000008; bitField0_ |= 0x00000010;
timestamp_ = value; timestamp_ = value;
onChanged(); onChanged();
return this; return this;
} }
public Builder clearTimestamp() { public Builder clearTimestamp() {
bitField0_ = (bitField0_ & ~0x00000008); bitField0_ = (bitField0_ & ~0x00000010);
timestamp_ = 0L; timestamp_ = 0L;
onChanged(); onChanged();
return this; return this;
@ -619,7 +676,7 @@ public final class PushMessageProtos {
// optional bytes message = 6; // optional bytes message = 6;
private com.google.protobuf.ByteString message_ = com.google.protobuf.ByteString.EMPTY; private com.google.protobuf.ByteString message_ = com.google.protobuf.ByteString.EMPTY;
public boolean hasMessage() { public boolean hasMessage() {
return ((bitField0_ & 0x00000010) == 0x00000010); return ((bitField0_ & 0x00000020) == 0x00000020);
} }
public com.google.protobuf.ByteString getMessage() { public com.google.protobuf.ByteString getMessage() {
return message_; return message_;
@ -628,13 +685,13 @@ public final class PushMessageProtos {
if (value == null) { if (value == null) {
throw new NullPointerException(); throw new NullPointerException();
} }
bitField0_ |= 0x00000010; bitField0_ |= 0x00000020;
message_ = value; message_ = value;
onChanged(); onChanged();
return this; return this;
} }
public Builder clearMessage() { public Builder clearMessage() {
bitField0_ = (bitField0_ & ~0x00000010); bitField0_ = (bitField0_ & ~0x00000020);
message_ = getDefaultInstance().getMessage(); message_ = getDefaultInstance().getMessage();
onChanged(); onChanged();
return this; return this;
@ -2800,23 +2857,24 @@ public final class PushMessageProtos {
static { static {
java.lang.String[] descriptorData = { java.lang.String[] descriptorData = {
"\n\037IncomingPushMessageSignal.proto\022\ntexts" + "\n\037IncomingPushMessageSignal.proto\022\ntexts" +
"ecure\"l\n\031IncomingPushMessageSignal\022\014\n\004ty" + "ecure\"\202\001\n\031IncomingPushMessageSignal\022\014\n\004t" +
"pe\030\001 \001(\r\022\016\n\006source\030\002 \001(\t\022\r\n\005relay\030\003 \001(\t\022" + "ype\030\001 \001(\r\022\016\n\006source\030\002 \001(\t\022\024\n\014sourceDevic" +
"\021\n\ttimestamp\030\005 \001(\004\022\017\n\007message\030\006 \001(\014\"\363\003\n\022" + "e\030\007 \001(\r\022\r\n\005relay\030\003 \001(\t\022\021\n\ttimestamp\030\005 \001(" +
"PushMessageContent\022\014\n\004body\030\001 \001(\t\022E\n\013atta" + "\004\022\017\n\007message\030\006 \001(\014\"\363\003\n\022PushMessageConten" +
"chments\030\002 \003(\01320.textsecure.PushMessageCo" + "t\022\014\n\004body\030\001 \001(\t\022E\n\013attachments\030\002 \003(\01320.t" +
"ntent.AttachmentPointer\022:\n\005group\030\003 \001(\0132+" + "extsecure.PushMessageContent.AttachmentP" +
".textsecure.PushMessageContent.GroupCont" + "ointer\022:\n\005group\030\003 \001(\0132+.textsecure.PushM" +
"ext\032A\n\021AttachmentPointer\022\n\n\002id\030\001 \001(\006\022\023\n\013" + "essageContent.GroupContext\032A\n\021Attachment" +
"contentType\030\002 \001(\t\022\013\n\003key\030\003 \001(\014\032\210\002\n\014Group", "Pointer\022\n\n\002id\030\001 \001(\006\022\023\n\013contentType\030\002 \001(\t",
"Context\022\n\n\002id\030\001 \001(\014\022>\n\004type\030\002 \001(\01620.text" + "\022\013\n\003key\030\003 \001(\014\032\210\002\n\014GroupContext\022\n\n\002id\030\001 \001" +
"secure.PushMessageContent.GroupContext.T" + "(\014\022>\n\004type\030\002 \001(\01620.textsecure.PushMessag" +
"ype\022\014\n\004name\030\003 \001(\t\022\017\n\007members\030\004 \003(\t\022@\n\006av" + "eContent.GroupContext.Type\022\014\n\004name\030\003 \001(\t" +
"atar\030\005 \001(\01320.textsecure.PushMessageConte" + "\022\017\n\007members\030\004 \003(\t\022@\n\006avatar\030\005 \001(\01320.text" +
"nt.AttachmentPointer\"K\n\004Type\022\013\n\007UNKNOWN\020" + "secure.PushMessageContent.AttachmentPoin" +
"\000\022\n\n\006CREATE\020\001\022\n\n\006MODIFY\020\002\022\013\n\007DELIVER\020\003\022\007" + "ter\"K\n\004Type\022\013\n\007UNKNOWN\020\000\022\n\n\006CREATE\020\001\022\n\n\006" +
"\n\003ADD\020\004\022\010\n\004QUIT\020\005B7\n\"org.whispersystems." + "MODIFY\020\002\022\013\n\007DELIVER\020\003\022\007\n\003ADD\020\004\022\010\n\004QUIT\020\005" +
"textsecure.pushB\021PushMessageProtos" "B7\n\"org.whispersystems.textsecure.pushB\021" +
"PushMessageProtos"
}; };
com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner = com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner =
new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() { new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() {
@ -2828,7 +2886,7 @@ public final class PushMessageProtos {
internal_static_textsecure_IncomingPushMessageSignal_fieldAccessorTable = new internal_static_textsecure_IncomingPushMessageSignal_fieldAccessorTable = new
com.google.protobuf.GeneratedMessage.FieldAccessorTable( com.google.protobuf.GeneratedMessage.FieldAccessorTable(
internal_static_textsecure_IncomingPushMessageSignal_descriptor, 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.class,
org.whispersystems.textsecure.push.PushMessageProtos.IncomingPushMessageSignal.Builder.class); org.whispersystems.textsecure.push.PushMessageProtos.IncomingPushMessageSignal.Builder.class);
internal_static_textsecure_PushMessageContent_descriptor = internal_static_textsecure_PushMessageContent_descriptor =

View File

@ -5,6 +5,7 @@ import android.util.Log;
import com.google.thoughtcrimegson.Gson; import com.google.thoughtcrimegson.Gson;
import com.google.thoughtcrimegson.JsonParseException; import com.google.thoughtcrimegson.JsonParseException;
import com.google.thoughtcrimegson.JsonSyntaxException;
import org.apache.http.conn.ssl.StrictHostnameVerifier; import org.apache.http.conn.ssl.StrictHostnameVerifier;
import org.whispersystems.textsecure.crypto.IdentityKey; 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 VERIFY_ACCOUNT_PATH = "/v1/accounts/code/%s";
private static final String REGISTER_GCM_PATH = "/v1/accounts/gcm/"; private static final String REGISTER_GCM_PATH = "/v1/accounts/gcm/";
private static final String PREKEY_PATH = "/v1/keys/%s"; 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_TOKENS_PATH = "/v1/directory/tokens";
private static final String DIRECTORY_VERIFY_PATH = "/v1/directory/%s"; 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 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 Context context;
private final String serviceUrl; private final String serviceUrl;
@ -86,44 +88,14 @@ public class PushServiceSocket {
makeRequest(REGISTER_GCM_PATH, "DELETE", null); makeRequest(REGISTER_GCM_PATH, "DELETE", null);
} }
public void sendMessage(PushDestination recipient, PushBody pushBody) public void sendMessage(OutgoingPushMessageList bundle)
throws IOException throws IOException
{ {
OutgoingPushMessage message = new OutgoingPushMessage(recipient.getRelay(), try {
recipient.getNumber(), makeRequest(String.format(MESSAGE_PATH, bundle.getDestination()), "PUT", new Gson().toJson(bundle));
pushBody.getBody(), } catch (NotFoundException nfe) {
pushBody.getType()); throw new UnregisteredUserException(nfe);
sendMessage(new OutgoingPushMessageList(message));
} }
public void sendMessage(List<PushDestination> recipients, List<PushBody> bodies)
throws IOException
{
List<OutgoingPushMessage> messages = new LinkedList<OutgoingPushMessage>();
Iterator<PushDestination> recipientsIterator = recipients.iterator();
Iterator<PushBody> 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()));
}
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, public void registerPreKeys(IdentityKey identityKey,
@ -150,20 +122,46 @@ public class PushServiceSocket {
PreKeyList.toJson(new PreKeyList(lastResortEntity, entities))); PreKeyList.toJson(new PreKeyList(lastResortEntity, entities)));
} }
public PreKeyEntity getPreKey(PushDestination destination) throws IOException { public List<PreKeyEntity> getPreKeys(PushAddress destination) throws IOException {
try { 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())) { if (!Util.isEmpty(destination.getRelay())) {
path = path + "?relay=" + destination.getRelay(); path = path + "?relay=" + destination.getRelay();
} }
String responseText = makeRequest(path, "GET", null); String responseText = makeRequest(path, "GET", null);
Log.w("PushServiceSocket", "Got prekey: " + responseText); PreKeyList response = PreKeyList.fromJson(responseText);
return PreKeyEntity.fromJson(responseText);
return response.getKeys();
} catch (JsonParseException e) { } catch (JsonParseException e) {
Log.w("PushServiceSocket", e); throw new IOException(e);
throw new IOException("Bad prekey"); }
}
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) { if (connection.getResponseCode() == 413) {
connection.disconnect();
throw new RateLimitException("Rate limit exceeded: " + connection.getResponseCode()); throw new RateLimitException("Rate limit exceeded: " + connection.getResponseCode());
} }
if (connection.getResponseCode() == 401 || connection.getResponseCode() == 403) { if (connection.getResponseCode() == 401 || connection.getResponseCode() == 403) {
connection.disconnect();
throw new AuthorizationFailedException("Authorization failed!"); throw new AuthorizationFailedException("Authorization failed!");
} }
if (connection.getResponseCode() == 404) { if (connection.getResponseCode() == 404) {
connection.disconnect();
throw new NotFoundException("Not found"); 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) { if (connection.getResponseCode() != 200 && connection.getResponseCode() != 204) {
throw new IOException("Bad response: " + connection.getResponseCode() + " " + connection.getResponseMessage()); throw new IOException("Bad response: " + connection.getResponseCode() + " " + connection.getResponseMessage());
} }

View File

@ -5,15 +5,8 @@ import java.util.List;
public class UnregisteredUserException extends IOException { public class UnregisteredUserException extends IOException {
private final List<String> addresses; public UnregisteredUserException(Exception exception) {
super(exception);
public UnregisteredUserException(List<String> addresses) {
super();
this.addresses = addresses;
}
public List<String> getAddresses() {
return addresses;
} }
} }

View File

@ -0,0 +1,6 @@
package org.whispersystems.textsecure.storage;
public interface CanonicalRecipient {
// public String getNumber();
public long getRecipientId();
}

View File

@ -1,7 +0,0 @@
package org.whispersystems.textsecure.storage;
import android.content.Context;
public interface CanonicalRecipientAddress {
public long getCanonicalAddress(Context context);
}

View File

@ -43,24 +43,24 @@ public class LocalKeyRecord extends Record {
private final MasterCipher masterCipher; private final MasterCipher masterCipher;
private final MasterSecret masterSecret; private final MasterSecret masterSecret;
public LocalKeyRecord(Context context, MasterSecret masterSecret, CanonicalRecipientAddress recipient) { public LocalKeyRecord(Context context, MasterSecret masterSecret, CanonicalRecipient recipient) {
super(context, SESSIONS_DIRECTORY, getFileNameForRecipient(context, recipient)); super(context, SESSIONS_DIRECTORY, getFileNameForRecipient(recipient));
this.masterSecret = masterSecret; this.masterSecret = masterSecret;
this.masterCipher = new MasterCipher(masterSecret); this.masterCipher = new MasterCipher(masterSecret);
loadData(); loadData();
} }
public static boolean hasRecord(Context context, CanonicalRecipientAddress recipient) { public static boolean hasRecord(Context context, CanonicalRecipient recipient) {
Log.w("LocalKeyRecord", "Checking: " + getFileNameForRecipient(context, recipient)); Log.w("LocalKeyRecord", "Checking: " + getFileNameForRecipient(recipient));
return Record.hasRecord(context, SESSIONS_DIRECTORY, getFileNameForRecipient(context, recipient)); return Record.hasRecord(context, SESSIONS_DIRECTORY, getFileNameForRecipient(recipient));
} }
public static void delete(Context context, CanonicalRecipientAddress recipient) { public static void delete(Context context, CanonicalRecipient recipient) {
Record.delete(context, SESSIONS_DIRECTORY, getFileNameForRecipient(context, recipient)); Record.delete(context, SESSIONS_DIRECTORY, getFileNameForRecipient(recipient));
} }
private static String getFileNameForRecipient(Context context, CanonicalRecipientAddress recipient) { private static String getFileNameForRecipient(CanonicalRecipient recipient) {
return recipient.getCanonicalAddress(context) + "-local"; return recipient.getRecipientId() + "-local";
} }
public void advanceKeyIfNecessary(int keyId) { public void advanceKeyIfNecessary(int keyId) {

View File

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

View File

@ -69,13 +69,19 @@ public abstract class Record {
} }
private static File getAddressFile(Context context, String directory, String address) { 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); File parent = new File(context.getFilesDir(), directory);
if (!parent.exists()) { if (!parent.exists()) {
parent.mkdirs(); parent.mkdirs();
} }
return new File(parent, address); return parent;
} }
protected byte[] readBlob(FileInputStream in) throws IOException { protected byte[] readBlob(FileInputStream in) throws IOException {

View File

@ -43,22 +43,22 @@ public class RemoteKeyRecord extends Record {
private PublicKey remoteKeyCurrent; private PublicKey remoteKeyCurrent;
private PublicKey remoteKeyLast; private PublicKey remoteKeyLast;
public RemoteKeyRecord(Context context, CanonicalRecipientAddress recipient) { public RemoteKeyRecord(Context context, CanonicalRecipient recipient) {
super(context, SESSIONS_DIRECTORY, getFileNameForRecipient(context, recipient)); super(context, SESSIONS_DIRECTORY, getFileNameForRecipient(recipient));
loadData(); loadData();
} }
public static void delete(Context context, CanonicalRecipientAddress recipient) { public static void delete(Context context, CanonicalRecipient recipient) {
delete(context, SESSIONS_DIRECTORY, getFileNameForRecipient(context, recipient)); delete(context, SESSIONS_DIRECTORY, getFileNameForRecipient(recipient));
} }
public static boolean hasRecord(Context context, CanonicalRecipientAddress recipient) { public static boolean hasRecord(Context context, CanonicalRecipient recipient) {
Log.w("LocalKeyRecord", "Checking: " + getFileNameForRecipient(context, recipient)); Log.w("LocalKeyRecord", "Checking: " + getFileNameForRecipient(recipient));
return hasRecord(context, SESSIONS_DIRECTORY, getFileNameForRecipient(context, recipient)); return hasRecord(context, SESSIONS_DIRECTORY, getFileNameForRecipient(recipient));
} }
private static String getFileNameForRecipient(Context context, CanonicalRecipientAddress recipient) { private static String getFileNameForRecipient(CanonicalRecipient recipient) {
return recipient.getCanonicalAddress(context) + "-remote"; return recipient.getRecipientId() + "-remote";
} }
public void updateCurrentRemoteKey(PublicKey remoteKey) { public void updateCurrentRemoteKey(PublicKey remoteKey) {

View File

@ -14,21 +14,21 @@ import org.whispersystems.textsecure.crypto.MasterSecret;
public class Session { 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. //XXX Obviously we should probably do something more thorough here eventually.
LocalKeyRecord.delete(context, recipient); LocalKeyRecord.delete(context, recipient);
RemoteKeyRecord.delete(context, recipient); RemoteKeyRecord.delete(context, recipient);
SessionRecordV1.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..."); Log.w("Session", "Aborting session, deleting keys...");
clearV1SessionFor(context, recipient); clearV1SessionFor(context, recipient);
SessionRecordV2.delete(context, recipient); SessionRecordV2.deleteAll(context, recipient);
} }
public static boolean hasSession(Context context, MasterSecret masterSecret, public static boolean hasSession(Context context, MasterSecret masterSecret,
CanonicalRecipientAddress recipient) CanonicalRecipient recipient)
{ {
Log.w("Session", "Checking session..."); Log.w("Session", "Checking session...");
return hasV1Session(context, recipient) || hasV2Session(context, masterSecret, recipient); return hasV1Session(context, recipient) || hasV2Session(context, masterSecret, recipient);
@ -36,42 +36,40 @@ public class Session {
public static boolean hasRemoteIdentityKey(Context context, public static boolean hasRemoteIdentityKey(Context context,
MasterSecret masterSecret, MasterSecret masterSecret,
CanonicalRecipientAddress recipient) CanonicalRecipient recipient)
{ {
return (hasV2Session(context, masterSecret, recipient) || return (hasV2Session(context, masterSecret, recipient) || (hasV1Session(context, recipient) &&
(hasV1Session(context, recipient) &&
new SessionRecordV1(context, masterSecret, recipient).getIdentityKey() != null)); new SessionRecordV1(context, masterSecret, recipient).getIdentityKey() != null));
} }
private static boolean hasV2Session(Context context, MasterSecret masterSecret, 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) && return SessionRecordV1.hasSession(context, recipient) &&
RemoteKeyRecord.hasRecord(context, recipient) && RemoteKeyRecord.hasRecord(context, recipient) &&
LocalKeyRecord.hasRecord(context, recipient); LocalKeyRecord.hasRecord(context, recipient);
} }
public static IdentityKey getRemoteIdentityKey(Context context, MasterSecret masterSecret, public static IdentityKey getRemoteIdentityKey(Context context, MasterSecret masterSecret,
CanonicalRecipientAddress recipient) CanonicalRecipient recipient)
{ {
if (SessionRecordV2.hasSession(context, masterSecret, recipient)) { return getRemoteIdentityKey(context, masterSecret, recipient.getRecipientId());
return new SessionRecordV2(context, masterSecret, recipient).getRemoteIdentityKey();
} else if (SessionRecordV1.hasSession(context, recipient)) {
return new SessionRecordV1(context, masterSecret, recipient).getIdentityKey();
} else {
return null;
}
} }
public static IdentityKey getRemoteIdentityKey(Context context, MasterSecret masterSecret, public static IdentityKey getRemoteIdentityKey(Context context,
MasterSecret masterSecret,
long recipientId) long recipientId)
{ {
if (SessionRecordV2.hasSession(context, masterSecret, recipientId)) { if (SessionRecordV2.hasSession(context, masterSecret, recipientId,
return new SessionRecordV2(context, masterSecret, recipientId).getRemoteIdentityKey(); RecipientDevice.DEFAULT_DEVICE_ID))
{
return new SessionRecordV2(context, masterSecret, recipientId,
RecipientDevice.DEFAULT_DEVICE_ID).getRemoteIdentityKey();
} else if (SessionRecordV1.hasSession(context, recipientId)) { } else if (SessionRecordV1.hasSession(context, recipientId)) {
return new SessionRecordV1(context, masterSecret, recipientId).getIdentityKey(); return new SessionRecordV1(context, masterSecret, recipientId).getIdentityKey();
} else { } else {
@ -80,10 +78,14 @@ public class Session {
} }
public static int getSessionVersion(Context context, MasterSecret masterSecret, public static int getSessionVersion(Context context, MasterSecret masterSecret,
CanonicalRecipientAddress recipient) CanonicalRecipient recipient)
{ {
if (SessionRecordV2.hasSession(context, masterSecret, recipient)) { if (SessionRecordV2.hasSession(context, masterSecret,
return new SessionRecordV2(context, masterSecret, recipient).getSessionVersion(); recipient.getRecipientId(),
RecipientDevice.DEFAULT_DEVICE_ID))
{
return new SessionRecordV2(context, masterSecret, recipient.getRecipientId(),
RecipientDevice.DEFAULT_DEVICE_ID).getSessionVersion();
} else if (SessionRecordV1.hasSession(context, recipient)) { } else if (SessionRecordV1.hasSession(context, recipient)) {
return new SessionRecordV1(context, masterSecret, recipient).getSessionVersion(); return new SessionRecordV1(context, masterSecret, recipient).getSessionVersion();
} }

View File

@ -37,8 +37,8 @@ public class SessionRecordV1 extends Record {
private final MasterSecret masterSecret; private final MasterSecret masterSecret;
public SessionRecordV1(Context context, MasterSecret masterSecret, CanonicalRecipientAddress recipient) { public SessionRecordV1(Context context, MasterSecret masterSecret, CanonicalRecipient recipient) {
this(context, masterSecret, getRecipientId(context, recipient)); this(context, masterSecret, recipient.getRecipientId());
} }
public SessionRecordV1(Context context, MasterSecret masterSecret, long recipientId) { public SessionRecordV1(Context context, MasterSecret masterSecret, long recipientId) {
@ -48,12 +48,12 @@ public class SessionRecordV1 extends Record {
loadData(); loadData();
} }
public static void delete(Context context, CanonicalRecipientAddress recipient) { public static void delete(Context context, CanonicalRecipient recipient) {
delete(context, SESSIONS_DIRECTORY, getRecipientId(context, recipient) + ""); delete(context, SESSIONS_DIRECTORY, recipient.getRecipientId() + "");
} }
public static boolean hasSession(Context context, CanonicalRecipientAddress recipient) { public static boolean hasSession(Context context, CanonicalRecipient recipient) {
return hasSession(context, getRecipientId(context, recipient)); return hasSession(context, recipient.getRecipientId());
} }
public static boolean hasSession(Context context, long recipientId) { public static boolean hasSession(Context context, long recipientId) {
@ -61,10 +61,6 @@ public class SessionRecordV1 extends Record {
return hasRecord(context, SESSIONS_DIRECTORY, recipientId+""); return hasRecord(context, SESSIONS_DIRECTORY, recipientId+"");
} }
private static long getRecipientId(Context context, CanonicalRecipientAddress recipient) {
return recipient.getCanonicalAddress(context);
}
public void setSessionKey(SessionKey sessionKeyRecord) { public void setSessionKey(SessionKey sessionKeyRecord) {
this.sessionKeyRecord = sessionKeyRecord; this.sessionKeyRecord = sessionKeyRecord;
} }

View File

@ -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.PendingKeyExchange;
import org.whispersystems.textsecure.storage.StorageProtos.SessionStructure.PendingPreKey; import org.whispersystems.textsecure.storage.StorageProtos.SessionStructure.PendingPreKey;
import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.RandomAccessFile; import java.io.RandomAccessFile;
import java.nio.channels.FileChannel; import java.nio.channels.FileChannel;
import java.util.Iterator; import java.util.Iterator;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import javax.crypto.spec.SecretKeySpec; import javax.crypto.spec.SecretKeySpec;
@ -64,33 +66,70 @@ public class SessionRecordV2 extends Record {
private StorageProtos.SessionStructure sessionStructure = private StorageProtos.SessionStructure sessionStructure =
StorageProtos.SessionStructure.newBuilder().build(); StorageProtos.SessionStructure.newBuilder().build();
public SessionRecordV2(Context context, MasterSecret masterSecret, CanonicalRecipientAddress recipient) { public SessionRecordV2(Context context, MasterSecret masterSecret, RecipientDevice peer) {
this(context, masterSecret, getRecipientId(context, recipient)); this(context, masterSecret, peer.getRecipientId(), peer.getDeviceId());
} }
public SessionRecordV2(Context context, MasterSecret masterSecret, long recipientId) { public SessionRecordV2(Context context, MasterSecret masterSecret, long recipientId, int deviceId) {
super(context, SESSIONS_DIRECTORY_V2, recipientId+""); super(context, SESSIONS_DIRECTORY_V2, getRecordName(recipientId, deviceId));
this.masterSecret = masterSecret; this.masterSecret = masterSecret;
loadData(); loadData();
} }
public static void delete(Context context, CanonicalRecipientAddress recipient) { private static String getRecordName(long recipientId, int deviceId) {
delete(context, SESSIONS_DIRECTORY_V2, getRecipientId(context, recipient) + ""); return recipientId + (deviceId == RecipientDevice.DEFAULT_DEVICE_ID ? "" : "." + deviceId);
}
public static List<Integer> getSessionSubDevices(Context context, CanonicalRecipient recipient) {
List<Integer> results = new LinkedList<Integer>();
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<Integer> 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, 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) { public static boolean hasSession(Context context, MasterSecret masterSecret,
return hasRecord(context, SESSIONS_DIRECTORY_V2, recipientId+"") && long recipientId, int deviceId)
new SessionRecordV2(context, masterSecret, recipientId).hasSenderChain(); {
} return hasRecord(context, SESSIONS_DIRECTORY_V2, getRecordName(recipientId, deviceId)) &&
new SessionRecordV2(context, masterSecret, recipientId, deviceId).hasSenderChain();
private static long getRecipientId(Context context, CanonicalRecipientAddress recipient) {
return recipient.getCanonicalAddress(context);
} }
public void clear() { public void clear() {

View File

@ -16,6 +16,8 @@
*/ */
package org.whispersystems.textsecure.util; package org.whispersystems.textsecure.util;
import java.io.IOException;
/** /**
* Utility for generating hex dumps. * Utility for generating hex dumps.
*/ */
@ -43,6 +45,36 @@ public class Hex {
return buf.toString(); return buf.toString();
} }
public static String toStringCondensed(byte[] bytes) {
StringBuffer buf = new StringBuffer();
for (int i=0;i<bytes.length;i++) {
appendHexChar(buf, bytes[i]);
}
return buf.toString();
}
public static byte[] fromStringCondensed(String encoded) throws IOException {
final char[] data = encoded.toCharArray();
final int len = data.length;
if ((len & 0x01) != 0) {
throw new IOException("Odd number of characters.");
}
final byte[] out = new byte[len >> 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) { public static String dump(byte[] bytes) {
return dump(bytes, 0, bytes.length); return dump(bytes, 0, bytes.length);
} }

View File

@ -19,15 +19,10 @@ package org.thoughtcrime.securesms;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter; import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.app.FragmentTransaction; import android.support.v4.app.FragmentTransaction;
import android.support.v4.view.ViewPager; 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;
import com.actionbarsherlock.app.ActionBar.Tab; import com.actionbarsherlock.app.ActionBar.Tab;
import com.actionbarsherlock.app.ActionBar.TabListener; import com.actionbarsherlock.app.ActionBar.TabListener;
@ -35,6 +30,14 @@ import com.actionbarsherlock.view.Menu;
import com.actionbarsherlock.view.MenuInflater; import com.actionbarsherlock.view.MenuInflater;
import com.actionbarsherlock.view.MenuItem; 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 * Activity container for selecting a list of contacts. Provides a tab frame for
* contact, group, and "recent contact" activity tabs. Used by ComposeMessageActivity * contact, group, and "recent contact" activity tabs. Used by ComposeMessageActivity
@ -52,8 +55,6 @@ public class ContactSelectionActivity extends PassphraseRequiredSherlockFragment
private ContactSelectionGroupsFragment groupsFragment; private ContactSelectionGroupsFragment groupsFragment;
private ContactSelectionRecentFragment recentFragment; private ContactSelectionRecentFragment recentFragment;
private Recipients recipients;
@Override @Override
protected void onCreate(Bundle icicle) { protected void onCreate(Bundle icicle) {
dynamicTheme.onCreate(this); dynamicTheme.onCreate(this);
@ -97,12 +98,12 @@ public class ContactSelectionActivity extends PassphraseRequiredSherlockFragment
} }
private void handleSelectionFinished() { private void handleSelectionFinished() {
recipients = contactsFragment.getSelectedContacts(); List<ContactData> contacts = contactsFragment.getSelectedContacts();
recipients.append(recentFragment.getSelectedContacts()); contacts.addAll(recentFragment.getSelectedContacts());
recipients.append(groupsFragment.getSelectedContacts(this)); contacts.addAll(groupsFragment.getSelectedContacts(this));
Intent resultIntent = getIntent(); Intent resultIntent = getIntent();
resultIntent.putExtra("recipients", this.recipients); resultIntent.putParcelableArrayListExtra("contacts", new ArrayList<ContactData>(contacts));
setResult(RESULT_OK, resultIntent); setResult(RESULT_OK, resultIntent);

View File

@ -84,23 +84,17 @@ public class ContactSelectionGroupsFragment extends SherlockListFragment
this.getListView().setFocusable(true); this.getListView().setFocusable(true);
} }
public Recipients getSelectedContacts(Context context) { public List<ContactData> getSelectedContacts(Context context) {
List<Recipient> recipientList = new LinkedList<Recipient>(); List<ContactData> contacts = new LinkedList<ContactData>();
for (GroupData groupData : selectedGroups.values()) { for (GroupData groupData : selectedGroups.values()) {
List<ContactData> contactDataList = ContactAccessor.getInstance() List<ContactData> contactDataList = ContactAccessor.getInstance()
.getGroupMembership(context, groupData.id); .getGroupMembership(context, groupData.id);
Log.w("GroupSelectionListActivity", "Got contacts in group: " + contactDataList.size()); contacts.addAll(contactDataList);
for (ContactData contactData : contactDataList) {
for (NumberData numberData : contactData.numbers) {
recipientList.add(new Recipient(contactData.name, numberData.number, null, null));
}
}
} }
return new Recipients(recipientList); return contacts;
} }
private void addGroup(GroupData groupData) { private void addGroup(GroupData groupData) {

View File

@ -45,6 +45,7 @@ import org.thoughtcrime.securesms.contacts.ContactAccessor.NumberData;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients; import org.thoughtcrime.securesms.recipients.Recipients;
import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
@ -96,16 +97,11 @@ public class ContactSelectionListFragment extends SherlockListFragment
} }
public Recipients getSelectedContacts() { public List<ContactData> getSelectedContacts() {
List<Recipient> recipientList = new LinkedList<Recipient>(); List<ContactData> contacts = new LinkedList<ContactData>();
contacts.addAll(selectedContacts.values());
for (ContactData contactData : selectedContacts.values()) { return contacts;
for (NumberData numberData : contactData.numbers) {
recipientList.add(new Recipient(contactData.name, numberData.number, null, null));
}
}
return new Recipients(recipientList);
} }

View File

@ -84,16 +84,11 @@ public class ContactSelectionRecentFragment extends SherlockListFragment
this.getLoaderManager().initLoader(0, null, this); this.getLoaderManager().initLoader(0, null, this);
} }
public Recipients getSelectedContacts() { public List<ContactData> getSelectedContacts() {
List<Recipient> recipientList = new LinkedList<Recipient>(); List<ContactData> contacts = new LinkedList<ContactData>();
contacts.addAll(selectedContacts.values());
for (ContactData contactData : selectedContacts.values()) { return contacts;
for (NumberData numberData : contactData.numbers) {
recipientList.add(new Recipient(contactData.name, numberData.number, null, null));
}
}
return new Recipients(recipientList);
} }
private void addSingleNumberContact(ContactData contactData) { private void addSingleNumberContact(ContactData contactData) {

View File

@ -212,10 +212,10 @@ public class ConversationActivity extends PassphraseRequiredSherlockFragmentActi
switch (reqCode) { switch (reqCode) {
case PICK_CONTACT: case PICK_CONTACT:
Recipients recipients = data.getParcelableExtra("recipients"); List<ContactData> contacts = data.getParcelableArrayListExtra("contacts");
if (recipients != null) if (contacts != null)
recipientsPanel.addRecipients(recipients); recipientsPanel.addContacts(contacts);
break; break;
case PICK_IMAGE: case PICK_IMAGE:
@ -912,7 +912,7 @@ public class ConversationActivity extends PassphraseRequiredSherlockFragmentActi
allocatedThreadId = MessageSender.sendMms(ConversationActivity.this, masterSecret, recipients, allocatedThreadId = MessageSender.sendMms(ConversationActivity.this, masterSecret, recipients,
threadId, attachmentManager.getSlideDeck(), body, threadId, attachmentManager.getSlideDeck(), body,
distributionType, isEncryptedConversation && !forcePlaintext); distributionType, isEncryptedConversation && !forcePlaintext);
} else if (recipients.isEmailRecipient() || !recipients.isSingleRecipient()) { } else if (recipients.isEmailRecipient() || !recipients.isSingleRecipient() || recipients.isGroupRecipient()) {
allocatedThreadId = MessageSender.sendMms(ConversationActivity.this, masterSecret, recipients, allocatedThreadId = MessageSender.sendMms(ConversationActivity.this, masterSecret, recipients,
threadId, new SlideDeck(), body, distributionType, threadId, new SlideDeck(), body, distributionType,
isEncryptedConversation && !forcePlaintext); isEncryptedConversation && !forcePlaintext);

View File

@ -347,6 +347,7 @@ public class ConversationItem extends LinearLayout {
private void handleKeyExchangeClicked() { private void handleKeyExchangeClicked() {
Intent intent = new Intent(context, ReceiveKeyActivity.class); Intent intent = new Intent(context, ReceiveKeyActivity.class);
intent.putExtra("recipient", messageRecord.getIndividualRecipient()); intent.putExtra("recipient", messageRecord.getIndividualRecipient());
intent.putExtra("recipient_device_id", messageRecord.getRecipientDeviceId());
intent.putExtra("body", messageRecord.getBody().getBody()); intent.putExtra("body", messageRecord.getBody().getBody());
intent.putExtra("thread_id", messageRecord.getThreadId()); intent.putExtra("thread_id", messageRecord.getThreadId());
intent.putExtra("message_id", messageRecord.getId()); intent.putExtra("message_id", messageRecord.getId());

View File

@ -38,6 +38,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.sms.SmsTransportDetails; import org.thoughtcrime.securesms.sms.SmsTransportDetails;
import org.thoughtcrime.securesms.util.MemoryCleaner; import org.thoughtcrime.securesms.util.MemoryCleaner;
import org.whispersystems.textsecure.crypto.IdentityKey;
import org.whispersystems.textsecure.crypto.InvalidKeyException; import org.whispersystems.textsecure.crypto.InvalidKeyException;
import org.whispersystems.textsecure.crypto.InvalidMessageException; import org.whispersystems.textsecure.crypto.InvalidMessageException;
import org.whispersystems.textsecure.crypto.InvalidVersionException; 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.CiphertextMessage;
import org.whispersystems.textsecure.crypto.protocol.PreKeyWhisperMessage; import org.whispersystems.textsecure.crypto.protocol.PreKeyWhisperMessage;
import org.whispersystems.textsecure.storage.InvalidKeyIdException; import org.whispersystems.textsecure.storage.InvalidKeyIdException;
import org.whispersystems.textsecure.storage.RecipientDevice;
import java.io.IOException; import java.io.IOException;
@ -62,6 +64,7 @@ public class ReceiveKeyActivity extends Activity {
private Button cancelButton; private Button cancelButton;
private Recipient recipient; private Recipient recipient;
private int recipientDeviceId;
private long threadId; private long threadId;
private long messageId; private long messageId;
@ -126,12 +129,14 @@ public class ReceiveKeyActivity extends Activity {
} }
private boolean isTrusted(KeyExchangeMessage message, PreKeyWhisperMessage messageBundle) { private boolean isTrusted(KeyExchangeMessage message, PreKeyWhisperMessage messageBundle) {
RecipientDevice recipientDevice = new RecipientDevice(recipient.getRecipientId(), recipientDeviceId);
if (message != null) { if (message != null) {
KeyExchangeProcessor processor = KeyExchangeProcessor.createFor(this, masterSecret, KeyExchangeProcessor processor = KeyExchangeProcessor.createFor(this, masterSecret,
recipient, message); recipientDevice, message);
return processor.isTrusted(message); return processor.isTrusted(message);
} else if (messageBundle != null) { } else if (messageBundle != null) {
KeyExchangeProcessorV2 processor = new KeyExchangeProcessorV2(this, masterSecret, recipient); KeyExchangeProcessorV2 processor = new KeyExchangeProcessorV2(this, masterSecret, recipientDevice);
return processor.isTrusted(messageBundle); return processor.isTrusted(messageBundle);
} }
@ -162,6 +167,7 @@ public class ReceiveKeyActivity extends Activity {
this.confirmButton = (Button) findViewById(R.id.ok_button); this.confirmButton = (Button) findViewById(R.id.ok_button);
this.cancelButton = (Button) findViewById(R.id.cancel_button); this.cancelButton = (Button) findViewById(R.id.cancel_button);
this.recipient = getIntent().getParcelableExtra("recipient"); this.recipient = getIntent().getParcelableExtra("recipient");
this.recipientDeviceId = getIntent().getIntExtra("recipient_device_id", -1);
this.threadId = getIntent().getLongExtra("thread_id", -1); this.threadId = getIntent().getLongExtra("thread_id", -1);
this.messageId = getIntent().getLongExtra("message_id", -1); this.messageId = getIntent().getLongExtra("message_id", -1);
this.masterSecret = getIntent().getParcelableExtra("master_secret"); this.masterSecret = getIntent().getParcelableExtra("master_secret");
@ -190,7 +196,8 @@ public class ReceiveKeyActivity extends Activity {
protected Void doInBackground(Void... params) { protected Void doInBackground(Void... params) {
if (keyExchangeMessage != null) { if (keyExchangeMessage != null) {
try { 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); processor.processKeyExchangeMessage(keyExchangeMessage, threadId);
DatabaseFactory.getEncryptingSmsDatabase(ReceiveKeyActivity.this) DatabaseFactory.getEncryptingSmsDatabase(ReceiveKeyActivity.this)
.markAsProcessedKeyExchange(messageId); .markAsProcessedKeyExchange(messageId);
@ -201,8 +208,9 @@ public class ReceiveKeyActivity extends Activity {
} }
} else if (keyExchangeMessageBundle != null) { } else if (keyExchangeMessageBundle != null) {
try { try {
RecipientDevice recipientDevice = new RecipientDevice(recipient.getRecipientId(), recipientDeviceId);
KeyExchangeProcessorV2 processor = new KeyExchangeProcessorV2(ReceiveKeyActivity.this, KeyExchangeProcessorV2 processor = new KeyExchangeProcessorV2(ReceiveKeyActivity.this,
masterSecret, recipient); masterSecret, recipientDevice);
processor.processKeyExchangeMessage(keyExchangeMessageBundle); processor.processKeyExchangeMessage(keyExchangeMessageBundle);
CiphertextMessage bundledMessage = keyExchangeMessageBundle.getWhisperMessage(); CiphertextMessage bundledMessage = keyExchangeMessageBundle.getWhisperMessage();
@ -213,8 +221,8 @@ public class ReceiveKeyActivity extends Activity {
.updateBundleMessageBody(masterSecret, messageId, messageBody); .updateBundleMessageBody(masterSecret, messageId, messageBody);
DecryptingQueue.scheduleDecryption(ReceiveKeyActivity.this, masterSecret, messageId, DecryptingQueue.scheduleDecryption(ReceiveKeyActivity.this, masterSecret, messageId,
threadId, recipient.getNumber(), messageBody, threadId, recipient.getNumber(), recipientDeviceId,
true, false); messageBody, true, false);
} catch (InvalidKeyIdException e) { } catch (InvalidKeyIdException e) {
Log.w("ReceiveKeyActivity", e); Log.w("ReceiveKeyActivity", e);
DatabaseFactory.getEncryptingSmsDatabase(ReceiveKeyActivity.this) DatabaseFactory.getEncryptingSmsDatabase(ReceiveKeyActivity.this)

View File

@ -24,6 +24,7 @@ import android.view.View;
import android.widget.RelativeLayout; import android.widget.RelativeLayout;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.contacts.ContactAccessor;
import org.thoughtcrime.securesms.contacts.RecipientsAdapter; import org.thoughtcrime.securesms.contacts.RecipientsAdapter;
import org.thoughtcrime.securesms.contacts.RecipientsEditor; import org.thoughtcrime.securesms.contacts.RecipientsEditor;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
@ -69,6 +70,14 @@ public class RecipientsPanel extends RelativeLayout {
else recipientsText.append(number + ", "); else recipientsText.append(number + ", ");
} }
public void addContacts(List<ContactAccessor.ContactData> contacts) {
for (ContactAccessor.ContactData contact : contacts) {
for (ContactAccessor.NumberData number : contact.numbers) {
addRecipient(contact.name, number.number);
}
}
}
public void addRecipients(Recipients recipients) { public void addRecipients(Recipients recipients) {
List<Recipient> recipientList = recipients.getRecipientsList(); List<Recipient> recipientList = recipients.getRecipientsList();
Iterator<Recipient> iterator = recipientList.iterator(); Iterator<Recipient> iterator = recipientList.iterator();

View File

@ -46,7 +46,9 @@ import org.whispersystems.textsecure.crypto.InvalidVersionException;
import org.whispersystems.textsecure.crypto.MasterSecret; import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.crypto.SessionCipher; import org.whispersystems.textsecure.crypto.SessionCipher;
import org.whispersystems.textsecure.push.IncomingPushMessage; import org.whispersystems.textsecure.push.IncomingPushMessage;
import org.whispersystems.textsecure.storage.RecipientDevice;
import org.whispersystems.textsecure.storage.Session; import org.whispersystems.textsecure.storage.Session;
import org.whispersystems.textsecure.storage.SessionRecordV2;
import org.whispersystems.textsecure.util.Hex; import org.whispersystems.textsecure.util.Hex;
import org.whispersystems.textsecure.util.Util; import org.whispersystems.textsecure.util.Util;
@ -78,11 +80,12 @@ public class DecryptingQueue {
} }
public static void scheduleDecryption(Context context, MasterSecret masterSecret, 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) String body, boolean isSecureMessage, boolean isKeyExchange)
{ {
DecryptionWorkItem runnable = new DecryptionWorkItem(context, masterSecret, messageId, threadId, DecryptionWorkItem runnable = new DecryptionWorkItem(context, masterSecret, messageId, threadId,
originator, body, isSecureMessage, isKeyExchange); originator, deviceId, body,
isSecureMessage, isKeyExchange);
executor.execute(runnable); executor.execute(runnable);
} }
@ -161,11 +164,13 @@ public class DecryptingQueue {
long threadId = record.getThreadId(); long threadId = record.getThreadId();
String body = record.getBody().getBody(); String body = record.getBody().getBody();
String originator = record.getIndividualRecipient().getNumber(); String originator = record.getIndividualRecipient().getNumber();
int originatorDeviceId = record.getRecipientDeviceId();
boolean isSecureMessage = record.isSecure(); boolean isSecureMessage = record.isSecure();
boolean isKeyExchange = record.isKeyExchange(); boolean isKeyExchange = record.isKeyExchange();
scheduleDecryption(context, masterSecret, messageId, threadId, scheduleDecryption(context, masterSecret, messageId, threadId,
originator, body, isSecureMessage, isKeyExchange); originator, originatorDeviceId, body,
isSecureMessage, isKeyExchange);
} }
private static class PushDecryptionWorkItem implements Runnable { private static class PushDecryptionWorkItem implements Runnable {
@ -188,13 +193,14 @@ public class DecryptingQueue {
try { try {
Recipients recipients = RecipientFactory.getRecipientsFromString(context, message.getSource(), false); Recipients recipients = RecipientFactory.getRecipientsFromString(context, message.getSource(), false);
Recipient recipient = recipients.getPrimaryRecipient(); 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); sendResult(PushReceiver.RESULT_NO_SESSION);
return; return;
} }
SessionCipher sessionCipher = SessionCipher.createFor(context, masterSecret, recipient); SessionCipher sessionCipher = SessionCipher.createFor(context, masterSecret, recipientDevice);
byte[] plaintextBody = sessionCipher.decrypt(message.getBody()); byte[] plaintextBody = sessionCipher.decrypt(message.getBody());
message = message.withBody(plaintextBody); message = message.withBody(plaintextBody);
@ -254,6 +260,7 @@ public class DecryptingQueue {
String messageFrom = pdu.getFrom().getString(); String messageFrom = pdu.getFrom().getString();
Recipients recipients = RecipientFactory.getRecipientsFromString(context, messageFrom, false); Recipients recipients = RecipientFactory.getRecipientsFromString(context, messageFrom, false);
Recipient recipient = recipients.getPrimaryRecipient(); Recipient recipient = recipients.getPrimaryRecipient();
RecipientDevice recipientDevice = new RecipientDevice(recipient.getRecipientId(), RecipientDevice.DEFAULT_DEVICE_ID);
byte[] ciphertextPduBytes = getEncryptedData(); byte[] ciphertextPduBytes = getEncryptedData();
if (ciphertextPduBytes == null) { if (ciphertextPduBytes == null) {
@ -272,7 +279,7 @@ public class DecryptingQueue {
Log.w("DecryptingQueue", "Decrypting: " + Hex.toString(ciphertextPduBytes)); Log.w("DecryptingQueue", "Decrypting: " + Hex.toString(ciphertextPduBytes));
TextTransport transportDetails = new TextTransport(); TextTransport transportDetails = new TextTransport();
SessionCipher sessionCipher = SessionCipher.createFor(context, masterSecret, recipient); SessionCipher sessionCipher = SessionCipher.createFor(context, masterSecret, recipientDevice);
byte[] decodedCiphertext = transportDetails.getDecodedMessage(ciphertextPduBytes); byte[] decodedCiphertext = transportDetails.getDecodedMessage(ciphertextPduBytes);
try { try {
@ -322,11 +329,13 @@ public class DecryptingQueue {
private final MasterSecret masterSecret; private final MasterSecret masterSecret;
private final String body; private final String body;
private final String originator; private final String originator;
private final int deviceId;
private final boolean isSecureMessage; private final boolean isSecureMessage;
private final boolean isKeyExchange; private final boolean isKeyExchange;
public DecryptionWorkItem(Context context, MasterSecret masterSecret, long messageId, long threadId, 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.context = context;
this.messageId = messageId; this.messageId = messageId;
@ -334,6 +343,7 @@ public class DecryptingQueue {
this.masterSecret = masterSecret; this.masterSecret = masterSecret;
this.body = body; this.body = body;
this.originator = originator; this.originator = originator;
this.deviceId = deviceId;
this.isSecureMessage = isSecureMessage; this.isSecureMessage = isSecureMessage;
this.isKeyExchange = isKeyExchange; this.isKeyExchange = isKeyExchange;
} }
@ -345,6 +355,7 @@ public class DecryptingQueue {
try { try {
Recipients recipients = RecipientFactory.getRecipientsFromString(context, originator, false); Recipients recipients = RecipientFactory.getRecipientsFromString(context, originator, false);
Recipient recipient = recipients.getPrimaryRecipient(); Recipient recipient = recipients.getPrimaryRecipient();
RecipientDevice recipientDevice = new RecipientDevice(recipient.getRecipientId(), deviceId);
if (!Session.hasSession(context, masterSecret, recipient)) { if (!Session.hasSession(context, masterSecret, recipient)) {
database.markAsNoSession(messageId); database.markAsNoSession(messageId);
@ -352,7 +363,7 @@ public class DecryptingQueue {
} }
SmsTransportDetails transportDetails = new SmsTransportDetails(); 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[] decodedCiphertext = transportDetails.getDecodedMessage(body.getBytes());
byte[] paddedPlaintext = sessionCipher.decrypt(decodedCiphertext); byte[] paddedPlaintext = sessionCipher.decrypt(decodedCiphertext);
@ -401,9 +412,10 @@ public class DecryptingQueue {
private void handleKeyExchangeProcessing(String plaintxtBody) { private void handleKeyExchangeProcessing(String plaintxtBody) {
if (TextSecurePreferences.isAutoRespondKeyExchangeEnabled(context)) { if (TextSecurePreferences.isAutoRespondKeyExchangeEnabled(context)) {
try { try {
Recipient recipient = new Recipient(null, originator, null, null); Recipient recipient = RecipientFactory.getRecipientsFromString(context, originator, false).getPrimaryRecipient();
RecipientDevice recipientDevice = new RecipientDevice(recipient.getRecipientId(), deviceId);
KeyExchangeMessage message = KeyExchangeMessage.createFor(plaintxtBody); KeyExchangeMessage message = KeyExchangeMessage.createFor(plaintxtBody);
KeyExchangeProcessor processor = KeyExchangeProcessor.createFor(context, masterSecret, recipient, message); KeyExchangeProcessor processor = KeyExchangeProcessor.createFor(context, masterSecret, recipientDevice, message);
if (processor.isStale(message)) { if (processor.isStale(message)) {
DatabaseFactory.getEncryptingSmsDatabase(context).markAsStaleKeyExchange(messageId); DatabaseFactory.getEncryptingSmsDatabase(context).markAsStaleKeyExchange(messageId);
@ -420,6 +432,9 @@ public class DecryptingQueue {
} catch (InvalidMessageException e) { } catch (InvalidMessageException e) {
Log.w("DecryptingQueue", e); Log.w("DecryptingQueue", e);
DatabaseFactory.getEncryptingSmsDatabase(context).markAsCorruptKeyExchange(messageId); DatabaseFactory.getEncryptingSmsDatabase(context).markAsCorruptKeyExchange(messageId);
} catch (RecipientFormattingException e) {
Log.w("DecryptingQueue", e);
DatabaseFactory.getEncryptingSmsDatabase(context).markAsCorruptKeyExchange(messageId);
} }
} }
} }

View File

@ -31,6 +31,7 @@ import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.crypto.ecc.Curve; import org.whispersystems.textsecure.crypto.ecc.Curve;
import org.whispersystems.textsecure.crypto.ecc.ECKeyPair; import org.whispersystems.textsecure.crypto.ecc.ECKeyPair;
import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage; import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage;
import org.whispersystems.textsecure.storage.RecipientDevice;
import org.whispersystems.textsecure.storage.SessionRecordV2; import org.whispersystems.textsecure.storage.SessionRecordV2;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
@ -70,8 +71,9 @@ public class KeyExchangeInitiator {
identityKey.getPublicKey()); identityKey.getPublicKey());
OutgoingKeyExchangeMessage textMessage = new OutgoingKeyExchangeMessage(recipient, message.serialize()); 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.setPendingKeyExchange(sequence, baseKey, ephemeralKey, identityKey);
sessionRecordV2.save(); sessionRecordV2.save();
@ -81,8 +83,9 @@ public class KeyExchangeInitiator {
private static boolean hasInitiatedSession(Context context, MasterSecret masterSecret, private static boolean hasInitiatedSession(Context context, MasterSecret masterSecret,
Recipient recipient) Recipient recipient)
{ {
RecipientDevice recipientDevice = new RecipientDevice(recipient.getRecipientId(), RecipientDevice.DEFAULT_DEVICE_ID);
return return
new SessionRecordV2(context, masterSecret, recipient) new SessionRecordV2(context, masterSecret, recipientDevice)
.hasPendingKeyExchange(); .hasPendingKeyExchange();
} }

View File

@ -21,8 +21,10 @@ import android.content.Context;
import org.thoughtcrime.securesms.crypto.protocol.KeyExchangeMessage; import org.thoughtcrime.securesms.crypto.protocol.KeyExchangeMessage;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.whispersystems.textsecure.crypto.InvalidMessageException; import org.whispersystems.textsecure.crypto.InvalidMessageException;
import org.whispersystems.textsecure.crypto.MasterSecret; import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.storage.RecipientDevice;
public abstract class KeyExchangeProcessor { public abstract class KeyExchangeProcessor {
@ -34,9 +36,13 @@ public abstract class KeyExchangeProcessor {
throws InvalidMessageException; throws InvalidMessageException;
public static KeyExchangeProcessor createFor(Context context, MasterSecret masterSecret, 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); if (message.isLegacy()) {
else return new KeyExchangeProcessorV2(context, masterSecret, recipient); return new KeyExchangeProcessorV1(context, masterSecret, recipientDevice.getRecipient());
} else {
return new KeyExchangeProcessorV2(context, masterSecret, recipientDevice);
}
} }
} }

View File

@ -8,6 +8,7 @@ import org.thoughtcrime.securesms.crypto.protocol.KeyExchangeMessage;
import org.thoughtcrime.securesms.crypto.protocol.KeyExchangeMessageV1; import org.thoughtcrime.securesms.crypto.protocol.KeyExchangeMessageV1;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.service.KeyCachingService; import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.sms.MessageSender; import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.sms.OutgoingKeyExchangeMessage; 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.MasterSecret;
import org.whispersystems.textsecure.crypto.ecc.Curve; import org.whispersystems.textsecure.crypto.ecc.Curve;
import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage; import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage;
import org.whispersystems.textsecure.storage.CanonicalRecipient;
import org.whispersystems.textsecure.storage.LocalKeyRecord; import org.whispersystems.textsecure.storage.LocalKeyRecord;
import org.whispersystems.textsecure.storage.RemoteKeyRecord; import org.whispersystems.textsecure.storage.RemoteKeyRecord;
import org.whispersystems.textsecure.storage.SessionRecordV1; import org.whispersystems.textsecure.storage.SessionRecordV1;
@ -33,13 +35,13 @@ import java.security.SecureRandom;
public class KeyExchangeProcessorV1 extends KeyExchangeProcessor { public class KeyExchangeProcessorV1 extends KeyExchangeProcessor {
private Context context; private Context context;
private Recipient recipient; private CanonicalRecipient recipient;
private MasterSecret masterSecret; private MasterSecret masterSecret;
private LocalKeyRecord localKeyRecord; private LocalKeyRecord localKeyRecord;
private RemoteKeyRecord remoteKeyRecord; private RemoteKeyRecord remoteKeyRecord;
private SessionRecordV1 sessionRecord; private SessionRecordV1 sessionRecord;
public KeyExchangeProcessorV1(Context context, MasterSecret masterSecret, Recipient recipient) { public KeyExchangeProcessorV1(Context context, MasterSecret masterSecret, CanonicalRecipient recipient) {
this.context = context; this.context = context;
this.recipient = recipient; this.recipient = recipient;
this.masterSecret = masterSecret; this.masterSecret = masterSecret;
@ -55,7 +57,8 @@ public class KeyExchangeProcessorV1 extends KeyExchangeProcessor {
} }
public boolean isTrusted(IdentityKey identityKey) { public boolean isTrusted(IdentityKey identityKey) {
return DatabaseFactory.getIdentityDatabase(context).isValidIdentity(masterSecret, recipient, return DatabaseFactory.getIdentityDatabase(context).isValidIdentity(masterSecret,
recipient.getRecipientId(),
identityKey); identityKey);
} }
@ -80,8 +83,13 @@ public class KeyExchangeProcessorV1 extends KeyExchangeProcessor {
@Override @Override
public void processKeyExchangeMessage(KeyExchangeMessage _message, long threadId) { public void processKeyExchangeMessage(KeyExchangeMessage _message, long threadId) {
KeyExchangeMessageV1 message = (KeyExchangeMessageV1)_message; KeyExchangeMessageV1 message = (KeyExchangeMessageV1) _message;
int initiateKeyId = Conversions.lowBitsToMedium(message.getRemoteKey().getId()); int initiateKeyId = Conversions.lowBitsToMedium(message.getRemoteKey().getId());
Recipient recipient = RecipientFactory.getRecipientsForIds(context,
this.recipient.getRecipientId()+"",
true).getPrimaryRecipient();
message.getRemoteKey().setId(initiateKeyId); message.getRemoteKey().setId(initiateKeyId);
if (needsResponseFromUs()) { if (needsResponseFromUs()) {
@ -113,7 +121,7 @@ public class KeyExchangeProcessorV1 extends KeyExchangeProcessor {
if (message.hasIdentityKey()) { if (message.hasIdentityKey()) {
DatabaseFactory.getIdentityDatabase(context) DatabaseFactory.getIdentityDatabase(context)
.saveIdentity(masterSecret, recipient, message.getIdentityKey()); .saveIdentity(masterSecret, recipient.getRecipientId(), message.getIdentityKey());
} }
DecryptingQueue.scheduleRogueMessages(context, masterSecret, recipient); DecryptingQueue.scheduleRogueMessages(context, masterSecret, recipient);
@ -130,7 +138,7 @@ public class KeyExchangeProcessorV1 extends KeyExchangeProcessor {
public LocalKeyRecord initializeRecordFor(Context context, public LocalKeyRecord initializeRecordFor(Context context,
MasterSecret masterSecret, MasterSecret masterSecret,
Recipient recipient) CanonicalRecipient recipient)
{ {
Log.w("KeyExchangeProcessorV1", "Initializing local key pairs..."); Log.w("KeyExchangeProcessorV1", "Initializing local key pairs...");
try { try {

View File

@ -8,6 +8,7 @@ import org.thoughtcrime.securesms.crypto.protocol.KeyExchangeMessage;
import org.thoughtcrime.securesms.crypto.protocol.KeyExchangeMessageV2; import org.thoughtcrime.securesms.crypto.protocol.KeyExchangeMessageV2;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.service.KeyCachingService; import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.sms.MessageSender; import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.sms.OutgoingKeyExchangeMessage; 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.push.PreKeyEntity;
import org.whispersystems.textsecure.storage.InvalidKeyIdException; import org.whispersystems.textsecure.storage.InvalidKeyIdException;
import org.whispersystems.textsecure.storage.PreKeyRecord; import org.whispersystems.textsecure.storage.PreKeyRecord;
import org.whispersystems.textsecure.storage.RecipientDevice;
import org.whispersystems.textsecure.storage.Session; import org.whispersystems.textsecure.storage.Session;
import org.whispersystems.textsecure.storage.SessionRecordV2; import org.whispersystems.textsecure.storage.SessionRecordV2;
import org.whispersystems.textsecure.util.Medium; import org.whispersystems.textsecure.util.Medium;
@ -37,15 +39,16 @@ import org.whispersystems.textsecure.util.Medium;
public class KeyExchangeProcessorV2 extends KeyExchangeProcessor { public class KeyExchangeProcessorV2 extends KeyExchangeProcessor {
private Context context; private Context context;
private Recipient recipient; private RecipientDevice recipientDevice;
private MasterSecret masterSecret; private MasterSecret masterSecret;
private SessionRecordV2 sessionRecord; private SessionRecordV2 sessionRecord;
public KeyExchangeProcessorV2(Context context, MasterSecret masterSecret, Recipient recipient) { public KeyExchangeProcessorV2(Context context, MasterSecret masterSecret, RecipientDevice recipientDevice)
{
this.context = context; this.context = context;
this.recipient = recipient; this.recipientDevice = recipientDevice;
this.masterSecret = masterSecret; this.masterSecret = masterSecret;
this.sessionRecord = new SessionRecordV2(context, masterSecret, recipient); this.sessionRecord = new SessionRecordV2(context, masterSecret, recipientDevice);
} }
public boolean isTrusted(PreKeyWhisperMessage message) { public boolean isTrusted(PreKeyWhisperMessage message) {
@ -57,7 +60,8 @@ public class KeyExchangeProcessorV2 extends KeyExchangeProcessor {
} }
public boolean isTrusted(IdentityKey identityKey) { public boolean isTrusted(IdentityKey identityKey) {
return DatabaseFactory.getIdentityDatabase(context).isValidIdentity(masterSecret, recipient, return DatabaseFactory.getIdentityDatabase(context).isValidIdentity(masterSecret,
recipientDevice.getRecipientId(),
identityKey); identityKey);
} }
@ -80,7 +84,7 @@ public class KeyExchangeProcessorV2 extends KeyExchangeProcessor {
Log.w("KeyExchangeProcessor", "Received pre-key with local key ID: " + preKeyId); 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..."); Log.w("KeyExchangeProcessor", "We've already processed the prekey part, letting bundled message fall through...");
return; return;
} }
@ -97,7 +101,7 @@ public class KeyExchangeProcessorV2 extends KeyExchangeProcessor {
RatchetingSession.initializeSession(sessionRecord, ourBaseKey, theirBaseKey, ourEphemeralKey, RatchetingSession.initializeSession(sessionRecord, ourBaseKey, theirBaseKey, ourEphemeralKey,
theirEphemeralKey, ourIdentityKey, theirIdentityKey); theirEphemeralKey, ourIdentityKey, theirIdentityKey);
Session.clearV1SessionFor(context, recipientDevice.getRecipient());
sessionRecord.save(); sessionRecord.save();
if (preKeyId != Medium.MAX_VALUE) { if (preKeyId != Medium.MAX_VALUE) {
@ -105,7 +109,7 @@ public class KeyExchangeProcessorV2 extends KeyExchangeProcessor {
} }
DatabaseFactory.getIdentityDatabase(context) DatabaseFactory.getIdentityDatabase(context)
.saveIdentity(masterSecret, recipient, theirIdentityKey); .saveIdentity(masterSecret, recipientDevice.getRecipientId(), theirIdentityKey);
} }
public void processKeyExchangeMessage(PreKeyEntity message, long threadId) public void processKeyExchangeMessage(PreKeyEntity message, long threadId)
@ -129,7 +133,7 @@ public class KeyExchangeProcessorV2 extends KeyExchangeProcessor {
sessionRecord.save(); sessionRecord.save();
DatabaseFactory.getIdentityDatabase(context) DatabaseFactory.getIdentityDatabase(context)
.saveIdentity(masterSecret, recipient, message.getIdentityKey()); .saveIdentity(masterSecret, recipientDevice.getRecipientId(), message.getIdentityKey());
broadcastSecurityUpdateEvent(context, threadId); broadcastSecurityUpdateEvent(context, threadId);
} }
@ -140,6 +144,10 @@ public class KeyExchangeProcessorV2 extends KeyExchangeProcessor {
{ {
try { try {
KeyExchangeMessageV2 message = (KeyExchangeMessageV2)_message; 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()); Log.w("KeyExchangeProcessorV2", "Received key exchange with sequence: " + message.getSequence());
@ -197,11 +205,11 @@ public class KeyExchangeProcessorV2 extends KeyExchangeProcessor {
ourIdentityKey, message.getIdentityKey()); ourIdentityKey, message.getIdentityKey());
sessionRecord.setSessionVersion(message.getVersion()); sessionRecord.setSessionVersion(message.getVersion());
Session.clearV1SessionFor(context, recipient); Session.clearV1SessionFor(context, recipientDevice.getRecipient());
sessionRecord.save(); sessionRecord.save();
DatabaseFactory.getIdentityDatabase(context) DatabaseFactory.getIdentityDatabase(context)
.saveIdentity(masterSecret, recipient, message.getIdentityKey()); .saveIdentity(masterSecret, recipientDevice.getRecipientId(), message.getIdentityKey());
DecryptingQueue.scheduleRogueMessages(context, masterSecret, recipient); DecryptingQueue.scheduleRogueMessages(context, masterSecret, recipient);

View File

@ -644,6 +644,9 @@ public class DatabaseFactory {
if (oldVersion < INTRODUCED_GROUP_DATABASE_VERSION) { 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 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("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(); db.setTransactionSuccessful();

View File

@ -6,11 +6,18 @@ import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteOpenHelper;
import android.graphics.Bitmap; 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.BitmapUtil;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.whispersystems.textsecure.util.Hex; import org.whispersystems.textsecure.util.Hex;
import org.whispersystems.textsecure.util.Util; import org.whispersystems.textsecure.util.Util;
import java.io.IOException;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; 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_ID = "avatar_id";
private static final String AVATAR_KEY = "avatar_key"; private static final String AVATAR_KEY = "avatar_key";
private static final String AVATAR_CONTENT_TYPE = "avatar_content_type"; 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"; private static final String TIMESTAMP = "timestamp";
public static final String CREATE_TABLE = public static final String CREATE_TABLE =
@ -42,6 +49,7 @@ public class GroupDatabase extends Database {
AVATAR_ID + " INTEGER, " + AVATAR_ID + " INTEGER, " +
AVATAR_KEY + " BLOB, " + AVATAR_KEY + " BLOB, " +
AVATAR_CONTENT_TYPE + " TEXT, " + AVATAR_CONTENT_TYPE + " TEXT, " +
AVATAR_RELAY + " TEXT, " +
TIMESTAMP + " INTEGER);"; TIMESTAMP + " INTEGER);";
public static final String[] CREATE_INDEXS = { public static final String[] CREATE_INDEXS = {
@ -52,19 +60,36 @@ public class GroupDatabase extends Database {
super(context, databaseHelper); super(context, databaseHelper);
} }
public Reader getGroup(String groupId) { public Reader getGroup(byte[] groupId) {
Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, null, GROUP_ID + " = ?", 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); return new Reader(cursor);
} }
public Recipients getGroupMembers(byte[] groupId) {
List<String> members = getCurrentMembers(groupId);
List<Recipient> recipients = new LinkedList<Recipient>();
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, public void create(byte[] groupId, String owner, String title,
List<String> members, AttachmentPointer avatar, List<String> members, AttachmentPointer avatar,
String relay) String relay)
{ {
ContentValues contentValues = new ContentValues(); ContentValues contentValues = new ContentValues();
contentValues.put(GROUP_ID, Hex.toString(groupId)); contentValues.put(GROUP_ID, GroupUtil.getEncodedId(groupId));
contentValues.put(OWNER, owner); contentValues.put(OWNER, owner);
contentValues.put(TITLE, title); contentValues.put(TITLE, title);
contentValues.put(MEMBERS, Util.join(members, ",")); contentValues.put(MEMBERS, Util.join(members, ","));
@ -75,7 +100,7 @@ public class GroupDatabase extends Database {
contentValues.put(AVATAR_CONTENT_TYPE, avatar.getContentType()); contentValues.put(AVATAR_CONTENT_TYPE, avatar.getContentType());
} }
contentValues.put(RELAY, relay); contentValues.put(AVATAR_RELAY, relay);
contentValues.put(TIMESTAMP, System.currentTimeMillis()); contentValues.put(TIMESTAMP, System.currentTimeMillis());
databaseHelper.getWritableDatabase().insert(TABLE_NAME, null, contentValues); databaseHelper.getWritableDatabase().insert(TABLE_NAME, null, contentValues);
@ -93,14 +118,15 @@ public class GroupDatabase extends Database {
databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues, databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues,
GROUP_ID + " = ? AND " + OWNER + " = ?", 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 contentValues = new ContentValues();
contentValues.put(AVATAR, BitmapUtil.toByteArray(avatar)); 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.getLong(cursor.getColumnIndexOrThrow(AVATAR_ID)),
cursor.getBlob(cursor.getColumnIndexOrThrow(AVATAR_KEY)), cursor.getBlob(cursor.getColumnIndexOrThrow(AVATAR_KEY)),
cursor.getString(cursor.getColumnIndexOrThrow(AVATAR_CONTENT_TYPE)), cursor.getString(cursor.getColumnIndexOrThrow(AVATAR_CONTENT_TYPE)),
cursor.getString(cursor.getColumnIndexOrThrow(RELAY))); cursor.getString(cursor.getColumnIndexOrThrow(AVATAR_RELAY)));
} }
public void close() { public void close() {
@ -206,8 +232,12 @@ public class GroupDatabase extends Database {
this.relay = relay; this.relay = relay;
} }
public String getId() { public byte[] getId() {
return id; try {
return GroupUtil.getDecodedId(id);
} catch (IOException ioe) {
throw new AssertionError(ioe);
}
} }
public String getTitle() { public String getTitle() {

View File

@ -67,12 +67,10 @@ public class IdentityDatabase extends Database {
} }
public boolean isValidIdentity(MasterSecret masterSecret, public boolean isValidIdentity(MasterSecret masterSecret,
Recipient recipient, long recipientId,
IdentityKey theirIdentity) IdentityKey theirIdentity)
{ {
SQLiteDatabase database = databaseHelper.getReadableDatabase(); SQLiteDatabase database = databaseHelper.getReadableDatabase();
String number = recipient.getNumber();
long recipientId = DatabaseFactory.getAddressDatabase(context).getCanonicalAddress(number);
MasterCipher masterCipher = new MasterCipher(masterSecret); MasterCipher masterCipher = new MasterCipher(masterSecret);
Cursor cursor = null; 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(); SQLiteDatabase database = databaseHelper.getWritableDatabase();
String number = recipient.getNumber();
long recipientId = DatabaseFactory.getAddressDatabase(context).getCanonicalAddress(number);
MasterCipher masterCipher = new MasterCipher(masterSecret); MasterCipher masterCipher = new MasterCipher(masterSecret);
String identityKeyString = Base64.encodeBytes(identityKey.serialize()); String identityKeyString = Base64.encodeBytes(identityKey.serialize());
String macString = Base64.encodeBytes(masterCipher.getMacFor(recipientId + String macString = Base64.encodeBytes(masterCipher.getMacFor(recipientId +

View File

@ -109,6 +109,7 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
READ + " INTEGER DEFAULT 0, " + MESSAGE_ID + " TEXT, " + SUBJECT + " TEXT, " + READ + " INTEGER DEFAULT 0, " + MESSAGE_ID + " TEXT, " + SUBJECT + " TEXT, " +
SUBJECT_CHARSET + " INTEGER, " + BODY + " TEXT, " + PART_COUNT + " INTEGER, " + SUBJECT_CHARSET + " INTEGER, " + BODY + " TEXT, " + PART_COUNT + " INTEGER, " +
CONTENT_TYPE + " TEXT, " + CONTENT_LOCATION + " TEXT, " + ADDRESS + " TEXT, " + CONTENT_TYPE + " TEXT, " + CONTENT_LOCATION + " TEXT, " + ADDRESS + " TEXT, " +
ADDRESS_DEVICE_ID + " INTEGER, " +
EXPIRY + " INTEGER, " + MESSAGE_CLASS + " TEXT, " + MESSAGE_TYPE + " INTEGER, " + EXPIRY + " INTEGER, " + MESSAGE_CLASS + " TEXT, " + MESSAGE_TYPE + " INTEGER, " +
MMS_VERSION + " INTEGER, " + MESSAGE_SIZE + " INTEGER, " + PRIORITY + " INTEGER, " + MMS_VERSION + " INTEGER, " + MESSAGE_SIZE + " INTEGER, " + PRIORITY + " INTEGER, " +
READ_REPORT + " INTEGER, " + REPORT_ALLOWED + " INTEGER, " + RESPONSE_STATUS + " 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, CONTENT_LOCATION, EXPIRY, MESSAGE_CLASS, MESSAGE_TYPE, MMS_VERSION,
MESSAGE_SIZE, PRIORITY, REPORT_ALLOWED, STATUS, TRANSACTION_ID, RETRIEVE_STATUS, MESSAGE_SIZE, PRIORITY, REPORT_ALLOWED, STATUS, TRANSACTION_ID, RETRIEVE_STATUS,
RETRIEVE_TEXT, RETRIEVE_TEXT_CS, READ_STATUS, CONTENT_CLASS, RESPONSE_TEXT, 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(); 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 threadId = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.THREAD_ID));
long mailbox = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.MESSAGE_BOX)); long mailbox = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.MESSAGE_BOX));
String address = cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.ADDRESS)); String address = cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.ADDRESS));
int addressDeviceId = cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.ADDRESS_DEVICE_ID));
Recipients recipients = getRecipientsFor(address); Recipients recipients = getRecipientsFor(address);
String contentLocation = cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.CONTENT_LOCATION)); 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(), return new NotificationMmsMessageRecord(context, id, recipients, recipients.getPrimaryRecipient(),
dateSent, dateReceived, threadId, contentLocationBytes, addressDeviceId, dateSent, dateReceived, threadId,
messageSize, expiry, status, transactionIdBytes, mailbox); contentLocationBytes, messageSize, expiry, status,
transactionIdBytes, mailbox);
} }
private MediaMmsMessageRecord getMediaMmsMessageRecord(Cursor cursor) { 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 box = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.MESSAGE_BOX));
long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.THREAD_ID)); long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.THREAD_ID));
String address = cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.ADDRESS)); String address = cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.ADDRESS));
int addressDeviceId = cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.ADDRESS_DEVICE_ID));
DisplayRecord.Body body = getBody(cursor); DisplayRecord.Body body = getBody(cursor);
int partCount = cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.PART_COUNT)); int partCount = cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.PART_COUNT));
Recipients recipients = getRecipientsFor(address); Recipients recipients = getRecipientsFor(address);
@ -825,29 +829,26 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
ListenableFutureTask<SlideDeck> slideDeck = getSlideDeck(masterSecret, id); ListenableFutureTask<SlideDeck> slideDeck = getSlideDeck(masterSecret, id);
return new MediaMmsMessageRecord(context, id, recipients, recipients.getPrimaryRecipient(), return new MediaMmsMessageRecord(context, id, recipients, recipients.getPrimaryRecipient(),
dateSent, dateReceived, threadId, body, addressDeviceId, dateSent, dateReceived, threadId, body,
slideDeck, partCount, box); slideDeck, partCount, box);
} }
private Recipients getRecipientsFor(String address) { private Recipients getRecipientsFor(String address) {
try { try {
if (Util.isEmpty(address) || address.equals("insert-address-token")) { if (Util.isEmpty(address) || address.equals("insert-address-token")) {
return new Recipients(new Recipient("Unknown", "Unknown", null, return new Recipients(Recipient.getUnknownRecipient(context));
ContactPhotoFactory.getDefaultContactPhoto(context)));
} }
Recipients recipients = RecipientFactory.getRecipientsFromString(context, address, false); Recipients recipients = RecipientFactory.getRecipientsFromString(context, address, false);
if (recipients == null || recipients.isEmpty()) { if (recipients == null || recipients.isEmpty()) {
return new Recipients(new Recipient("Unknown", "Unknown", null, return new Recipients(Recipient.getUnknownRecipient(context));
ContactPhotoFactory.getDefaultContactPhoto(context)));
} }
return recipients; return recipients;
} catch (RecipientFormattingException e) { } catch (RecipientFormattingException e) {
Log.w("MmsDatabase", e); Log.w("MmsDatabase", e);
return new Recipients(new Recipient("Unknown", "Unknown", null, return new Recipients(Recipient.getUnknownRecipient(context));
ContactPhotoFactory.getDefaultContactPhoto(context)));
} }
} }

View File

@ -9,6 +9,7 @@ public interface MmsSmsColumns {
public static final String READ = "read"; public static final String READ = "read";
public static final String BODY = "body"; 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 { public static class Types {

View File

@ -42,7 +42,7 @@ public class MmsSmsDatabase extends Database {
public Cursor getConversation(long threadId) { public Cursor getConversation(long threadId) {
String[] projection = {MmsSmsColumns.ID, SmsDatabase.BODY, SmsDatabase.TYPE, String[] projection = {MmsSmsColumns.ID, SmsDatabase.BODY, SmsDatabase.TYPE,
MmsSmsColumns.THREAD_ID, MmsSmsColumns.THREAD_ID,
SmsDatabase.ADDRESS, SmsDatabase.SUBJECT, SmsDatabase.ADDRESS, SmsDatabase.ADDRESS_DEVICE_ID, SmsDatabase.SUBJECT,
MmsSmsColumns.NORMALIZED_DATE_SENT, MmsSmsColumns.NORMALIZED_DATE_SENT,
MmsSmsColumns.NORMALIZED_DATE_RECEIVED, MmsSmsColumns.NORMALIZED_DATE_RECEIVED,
MmsDatabase.MESSAGE_TYPE, MmsDatabase.MESSAGE_BOX, MmsDatabase.MESSAGE_TYPE, MmsDatabase.MESSAGE_BOX,
@ -64,7 +64,7 @@ public class MmsSmsDatabase extends Database {
public Cursor getConversationSnippet(long threadId) { public Cursor getConversationSnippet(long threadId) {
String[] projection = {MmsSmsColumns.ID, SmsDatabase.BODY, SmsDatabase.TYPE, String[] projection = {MmsSmsColumns.ID, SmsDatabase.BODY, SmsDatabase.TYPE,
MmsSmsColumns.THREAD_ID, MmsSmsColumns.THREAD_ID,
SmsDatabase.ADDRESS, SmsDatabase.SUBJECT, SmsDatabase.ADDRESS, SmsDatabase.ADDRESS_DEVICE_ID, SmsDatabase.SUBJECT,
MmsSmsColumns.NORMALIZED_DATE_SENT, MmsSmsColumns.NORMALIZED_DATE_SENT,
MmsSmsColumns.NORMALIZED_DATE_RECEIVED, MmsSmsColumns.NORMALIZED_DATE_RECEIVED,
MmsDatabase.MESSAGE_TYPE, MmsDatabase.MESSAGE_BOX, MmsDatabase.MESSAGE_TYPE, MmsDatabase.MESSAGE_BOX,
@ -81,7 +81,7 @@ public class MmsSmsDatabase extends Database {
public Cursor getUnread() { public Cursor getUnread() {
String[] projection = {MmsSmsColumns.ID, SmsDatabase.BODY, SmsDatabase.READ, SmsDatabase.TYPE, 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, SmsDatabase.STATUS,
MmsSmsColumns.NORMALIZED_DATE_SENT, MmsSmsColumns.NORMALIZED_DATE_SENT,
MmsSmsColumns.NORMALIZED_DATE_RECEIVED, MmsSmsColumns.NORMALIZED_DATE_RECEIVED,
@ -108,7 +108,7 @@ public class MmsSmsDatabase extends Database {
String[] mmsProjection = {MmsDatabase.DATE_SENT + " * 1000 AS " + MmsSmsColumns.NORMALIZED_DATE_SENT, String[] mmsProjection = {MmsDatabase.DATE_SENT + " * 1000 AS " + MmsSmsColumns.NORMALIZED_DATE_SENT,
MmsDatabase.DATE_RECEIVED + " * 1000 AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED, MmsDatabase.DATE_RECEIVED + " * 1000 AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED,
MmsSmsColumns.ID, SmsDatabase.BODY, MmsSmsColumns.READ, MmsSmsColumns.THREAD_ID, 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.MESSAGE_BOX, SmsDatabase.STATUS, MmsDatabase.PART_COUNT,
MmsDatabase.CONTENT_LOCATION, MmsDatabase.TRANSACTION_ID, MmsDatabase.CONTENT_LOCATION, MmsDatabase.TRANSACTION_ID,
MmsDatabase.MESSAGE_SIZE, MmsDatabase.EXPIRY, MmsDatabase.STATUS, 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, String[] smsProjection = {SmsDatabase.DATE_SENT + " * 1 AS " + MmsSmsColumns.NORMALIZED_DATE_SENT,
SmsDatabase.DATE_RECEIVED + " * 1 AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED, SmsDatabase.DATE_RECEIVED + " * 1 AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED,
MmsSmsColumns.ID, SmsDatabase.BODY, MmsSmsColumns.READ, MmsSmsColumns.THREAD_ID, 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.MESSAGE_BOX, SmsDatabase.STATUS, MmsDatabase.PART_COUNT,
MmsDatabase.CONTENT_LOCATION, MmsDatabase.TRANSACTION_ID, MmsDatabase.CONTENT_LOCATION, MmsDatabase.TRANSACTION_ID,
MmsDatabase.MESSAGE_SIZE, MmsDatabase.EXPIRY, MmsDatabase.STATUS, MmsDatabase.MESSAGE_SIZE, MmsDatabase.EXPIRY, MmsDatabase.STATUS,
@ -139,6 +139,7 @@ public class MmsSmsDatabase extends Database {
mmsColumnsPresent.add(MmsSmsColumns.THREAD_ID); mmsColumnsPresent.add(MmsSmsColumns.THREAD_ID);
mmsColumnsPresent.add(MmsSmsColumns.BODY); mmsColumnsPresent.add(MmsSmsColumns.BODY);
mmsColumnsPresent.add(MmsSmsColumns.ADDRESS); mmsColumnsPresent.add(MmsSmsColumns.ADDRESS);
mmsColumnsPresent.add(MmsSmsColumns.ADDRESS_DEVICE_ID);
mmsColumnsPresent.add(MmsDatabase.MESSAGE_TYPE); mmsColumnsPresent.add(MmsDatabase.MESSAGE_TYPE);
mmsColumnsPresent.add(MmsDatabase.MESSAGE_BOX); mmsColumnsPresent.add(MmsDatabase.MESSAGE_BOX);
mmsColumnsPresent.add(MmsDatabase.DATE_SENT); mmsColumnsPresent.add(MmsDatabase.DATE_SENT);
@ -154,6 +155,7 @@ public class MmsSmsDatabase extends Database {
smsColumnsPresent.add(MmsSmsColumns.ID); smsColumnsPresent.add(MmsSmsColumns.ID);
smsColumnsPresent.add(MmsSmsColumns.BODY); smsColumnsPresent.add(MmsSmsColumns.BODY);
smsColumnsPresent.add(MmsSmsColumns.ADDRESS); smsColumnsPresent.add(MmsSmsColumns.ADDRESS);
smsColumnsPresent.add(MmsSmsColumns.ADDRESS_DEVICE_ID);
smsColumnsPresent.add(MmsSmsColumns.READ); smsColumnsPresent.add(MmsSmsColumns.READ);
smsColumnsPresent.add(MmsSmsColumns.THREAD_ID); smsColumnsPresent.add(MmsSmsColumns.THREAD_ID);
smsColumnsPresent.add(SmsDatabase.TYPE); smsColumnsPresent.add(SmsDatabase.TYPE);

View File

@ -18,11 +18,12 @@ public class PushDatabase extends Database {
public static final String ID = "_id"; public static final String ID = "_id";
public static final String TYPE = "type"; public static final String TYPE = "type";
public static final String SOURCE = "source"; public static final String SOURCE = "source";
public static final String DEVICE_ID = "device_id";
public static final String BODY = "body"; public static final String BODY = "body";
public static final String TIMESTAMP = "timestamp"; public static final String TIMESTAMP = "timestamp";
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " + 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) { public PushDatabase(Context context, SQLiteOpenHelper databaseHelper) {
super(context, databaseHelper); super(context, databaseHelper);
@ -64,10 +65,11 @@ public class PushDatabase extends Database {
int type = cursor.getInt(cursor.getColumnIndexOrThrow(TYPE)); int type = cursor.getInt(cursor.getColumnIndexOrThrow(TYPE));
String source = cursor.getString(cursor.getColumnIndexOrThrow(SOURCE)); String source = cursor.getString(cursor.getColumnIndexOrThrow(SOURCE));
int deviceId = cursor.getInt(cursor.getColumnIndexOrThrow(DEVICE_ID));
byte[] body = Base64.decode(cursor.getString(cursor.getColumnIndexOrThrow(BODY))); byte[] body = Base64.decode(cursor.getString(cursor.getColumnIndexOrThrow(BODY)));
long timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(TIMESTAMP)); 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) { } catch (IOException e) {
throw new AssertionError(e); throw new AssertionError(e);
} }

View File

@ -63,8 +63,8 @@ public class SmsDatabase extends Database implements MmsSmsColumns {
public static final String SERVICE_CENTER = "service_center"; public static final String SERVICE_CENTER = "service_center";
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " integer PRIMARY KEY, " + public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " integer PRIMARY KEY, " +
THREAD_ID + " INTEGER, " + ADDRESS + " TEXT, " + PERSON + " INTEGER, " + DATE_RECEIVED + " INTEGER, " + THREAD_ID + " INTEGER, " + ADDRESS + " TEXT, " + ADDRESS_DEVICE_ID + " INTEGER DEFAULT 1, " + PERSON + " INTEGER, " +
DATE_SENT + " INTEGER, " + PROTOCOL + " INTEGER, " + READ + " INTEGER DEFAULT 0, " + DATE_RECEIVED + " INTEGER, " + DATE_SENT + " INTEGER, " + PROTOCOL + " INTEGER, " + READ + " INTEGER DEFAULT 0, " +
STATUS + " INTEGER DEFAULT -1," + TYPE + " INTEGER, " + REPLY_PATH_PRESENT + " INTEGER, " + STATUS + " INTEGER DEFAULT -1," + TYPE + " INTEGER, " + REPLY_PATH_PRESENT + " INTEGER, " +
SUBJECT + " TEXT, " + BODY + " TEXT, " + SERVICE_CENTER + " TEXT);"; 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[] { 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_RECEIVED + " AS " + NORMALIZED_DATE_RECEIVED,
DATE_SENT + " AS " + NORMALIZED_DATE_SENT, DATE_SENT + " AS " + NORMALIZED_DATE_SENT,
PROTOCOL, READ, STATUS, TYPE, PROTOCOL, READ, STATUS, TYPE,
@ -257,19 +257,39 @@ public class SmsDatabase extends Database implements MmsSmsColumns {
type |= Types.ENCRYPTION_REMOTE_BIT; type |= Types.ENCRYPTION_REMOTE_BIT;
} }
Recipient recipient = new Recipient(null, message.getSender(), null, null); Recipients recipients;
Recipients recipients = new Recipients(recipient);
String groupId = message.getGroupId(); 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) || boolean unread = org.thoughtcrime.securesms.util.Util.isDefaultSmsProvider(context) ||
message.isSecureMessage() || message.isKeyExchange(); message.isSecureMessage() || message.isKeyExchange();
long threadId; long threadId;
if (groupId == null) threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipients); if (groupRecipients == null) threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipients);
else threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdForGroup(groupId); else threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipients);
ContentValues values = new ContentValues(6); ContentValues values = new ContentValues(6);
values.put(ADDRESS, message.getSender()); values.put(ADDRESS, message.getSender());
values.put(ADDRESS_DEVICE_ID, message.getSenderDeviceId());
values.put(DATE_RECEIVED, System.currentTimeMillis()); values.put(DATE_RECEIVED, System.currentTimeMillis());
values.put(DATE_SENT, message.getSentTimestampMillis()); values.put(DATE_SENT, message.getSentTimestampMillis());
values.put(PROTOCOL, message.getProtocol()); values.put(PROTOCOL, message.getProtocol());
@ -468,6 +488,7 @@ public class SmsDatabase extends Database implements MmsSmsColumns {
public SmsMessageRecord getCurrent() { public SmsMessageRecord getCurrent() {
long messageId = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.ID)); long messageId = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.ID));
String address = cursor.getString(cursor.getColumnIndexOrThrow(SmsDatabase.ADDRESS)); 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 type = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.TYPE));
long dateReceived = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.NORMALIZED_DATE_RECEIVED)); long dateReceived = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.NORMALIZED_DATE_RECEIVED));
long dateSent = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.NORMALIZED_DATE_SENT)); 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, return new SmsMessageRecord(context, messageId, body, recipients,
recipients.getPrimaryRecipient(), recipients.getPrimaryRecipient(),
addressDeviceId,
dateSent, dateReceived, type, dateSent, dateReceived, type,
threadId, status); threadId, status);
} }
@ -487,15 +509,13 @@ public class SmsDatabase extends Database implements MmsSmsColumns {
Recipients recipients = RecipientFactory.getRecipientsFromString(context, address, false); Recipients recipients = RecipientFactory.getRecipientsFromString(context, address, false);
if (recipients == null || recipients.isEmpty()) { if (recipients == null || recipients.isEmpty()) {
return new Recipients(new Recipient("Unknown", "Unknown", null, return new Recipients(Recipient.getUnknownRecipient(context));
ContactPhotoFactory.getDefaultContactPhoto(context)));
} }
return recipients; return recipients;
} catch (RecipientFormattingException e) { } catch (RecipientFormattingException e) {
Log.w("EncryptingSmsDatabase", e); Log.w("EncryptingSmsDatabase", e);
return new Recipients(new Recipient("Unknown", "Unknown", null, return new Recipients(Recipient.getUnknownRecipient(context));
ContactPhotoFactory.getDefaultContactPhoto(context)));
} }
} }

View File

@ -74,9 +74,7 @@ public class ThreadDatabase extends Database {
List<Recipient> recipientList = recipients.getRecipientsList(); List<Recipient> recipientList = recipients.getRecipientsList();
for (Recipient recipient : recipientList) { for (Recipient recipient : recipientList) {
// String number = NumberUtil.filterNumber(recipient.getNumber()); recipientSet.add(recipient.getRecipientId());
String number = recipient.getNumber();
recipientSet.add(Long.valueOf(DatabaseFactory.getAddressDatabase(context).getCanonicalAddress(number)));
} }
long[] recipientArray = new long[recipientSet.size()]; long[] recipientArray = new long[recipientSet.size()];

View File

@ -41,12 +41,13 @@ public class MediaMmsMessageRecord extends MessageRecord {
private final ListenableFutureTask<SlideDeck> slideDeck; private final ListenableFutureTask<SlideDeck> slideDeck;
public MediaMmsMessageRecord(Context context, long id, Recipients recipients, public MediaMmsMessageRecord(Context context, long id, Recipients recipients,
Recipient individualRecipient, long dateSent, long dateReceived, Recipient individualRecipient, int recipientDeviceId,
long threadId, Body body, ListenableFutureTask<SlideDeck> slideDeck, long dateSent, long dateReceived, long threadId, Body body,
ListenableFutureTask<SlideDeck> slideDeck,
int partCount, long mailbox) int partCount, long mailbox)
{ {
super(context, id, body, recipients, individualRecipient, dateSent, dateReceived, super(context, id, body, recipients, individualRecipient, recipientDeviceId,
threadId, DELIVERY_STATUS_NONE, mailbox); dateSent, dateReceived, threadId, DELIVERY_STATUS_NONE, mailbox);
this.context = context.getApplicationContext(); this.context = context.getApplicationContext();
this.partCount = partCount; this.partCount = partCount;

View File

@ -45,11 +45,12 @@ public abstract class MessageRecord extends DisplayRecord {
public static final int DELIVERY_STATUS_FAILED = 3; public static final int DELIVERY_STATUS_FAILED = 3;
private final Recipient individualRecipient; private final Recipient individualRecipient;
private final int recipientDeviceId;
private final long id; private final long id;
private final int deliveryStatus; private final int deliveryStatus;
public MessageRecord(Context context, long id, Body body, Recipients recipients, MessageRecord(Context context, long id, Body body, Recipients recipients,
Recipient individualRecipient, Recipient individualRecipient, int recipientDeviceId,
long dateSent, long dateReceived, long dateSent, long dateReceived,
long threadId, int deliveryStatus, long threadId, int deliveryStatus,
long type) long type)
@ -57,6 +58,7 @@ public abstract class MessageRecord extends DisplayRecord {
super(context, body, recipients, dateSent, dateReceived, threadId, type); super(context, body, recipients, dateSent, dateReceived, threadId, type);
this.id = id; this.id = id;
this.individualRecipient = individualRecipient; this.individualRecipient = individualRecipient;
this.recipientDeviceId = recipientDeviceId;
this.deliveryStatus = deliveryStatus; this.deliveryStatus = deliveryStatus;
} }
@ -121,6 +123,10 @@ public abstract class MessageRecord extends DisplayRecord {
return individualRecipient; return individualRecipient;
} }
public int getRecipientDeviceId() {
return recipientDeviceId;
}
public long getType() { public long getType() {
return type; return type;
} }

View File

@ -41,13 +41,13 @@ public class NotificationMmsMessageRecord extends MessageRecord {
private final byte[] transactionId; private final byte[] transactionId;
public NotificationMmsMessageRecord(Context context, long id, Recipients recipients, public NotificationMmsMessageRecord(Context context, long id, Recipients recipients,
Recipient individualRecipient, Recipient individualRecipient, int recipientDeviceId,
long dateSent, long dateReceived, long threadId, long dateSent, long dateReceived, long threadId,
byte[] contentLocation, long messageSize, long expiry, byte[] contentLocation, long messageSize, long expiry,
int status, byte[] transactionId, long mailbox) int status, byte[] transactionId, long mailbox)
{ {
super(context, id, new Body("", true), recipients, individualRecipient, dateSent, dateReceived, super(context, id, new Body("", true), recipients, individualRecipient, recipientDeviceId,
threadId, DELIVERY_STATUS_NONE, mailbox); dateSent, dateReceived, threadId, DELIVERY_STATUS_NONE, mailbox);
this.contentLocation = contentLocation; this.contentLocation = contentLocation;
this.messageSize = messageSize; this.messageSize = messageSize;

View File

@ -38,12 +38,13 @@ public class SmsMessageRecord extends MessageRecord {
public SmsMessageRecord(Context context, long id, public SmsMessageRecord(Context context, long id,
Body body, Recipients recipients, Body body, Recipients recipients,
Recipient individualRecipient, Recipient individualRecipient,
int recipientDeviceId,
long dateSent, long dateReceived, long dateSent, long dateReceived,
long type, long threadId, long type, long threadId,
int status) int status)
{ {
super(context, id, body, recipients, individualRecipient, dateSent, dateReceived, super(context, id, body, recipients, individualRecipient, recipientDeviceId,
threadId, getGenericDeliveryStatus(status), type); dateSent, dateReceived, threadId, getGenericDeliveryStatus(status), type);
} }
public long getType() { public long getType() {

View File

@ -1,11 +1,13 @@
package org.thoughtcrime.securesms.mms; package org.thoughtcrime.securesms.mms;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.textsecure.crypto.MasterCipher; import org.whispersystems.textsecure.crypto.MasterCipher;
import org.whispersystems.textsecure.crypto.MasterSecret; import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.push.IncomingPushMessage; import org.whispersystems.textsecure.push.IncomingPushMessage;
import org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent; import org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent;
import org.whispersystems.textsecure.util.Base64; 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.EncodedStringValue;
import ws.com.google.android.mms.pdu.PduBody; import ws.com.google.android.mms.pdu.PduBody;
@ -27,12 +29,16 @@ public class IncomingMediaMessage {
public IncomingMediaMessage(MasterSecret masterSecret, String localNumber, public IncomingMediaMessage(MasterSecret masterSecret, String localNumber,
IncomingPushMessage message, IncomingPushMessage message,
PushMessageContent messageContent, PushMessageContent messageContent)
String groupId)
{ {
this.headers = new PduHeaders(); this.headers = new PduHeaders();
this.body = new PduBody(); 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.setEncodedStringValue(new EncodedStringValue(message.getSource()), PduHeaders.FROM);
this.headers.appendEncodedStringValue(new EncodedStringValue(localNumber), PduHeaders.TO); this.headers.appendEncodedStringValue(new EncodedStringValue(localNumber), PduHeaders.TO);

View File

@ -296,7 +296,7 @@ public class MessageNotifier {
recipient = RecipientFactory.getRecipientsFromString(context, message.getSource(), false).getPrimaryRecipient(); recipient = RecipientFactory.getRecipientsFromString(context, message.getSource(), false).getPrimaryRecipient();
} catch (RecipientFormattingException e) { } catch (RecipientFormattingException e) {
Log.w("MessageNotifier", 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); Recipients recipients = RecipientFactory.getRecipientsFromMessage(context, message, false);

View File

@ -23,15 +23,16 @@ import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import android.util.Log; 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.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.FutureTaskListener;
import org.whispersystems.textsecure.util.ListenableFutureTask; import org.whispersystems.textsecure.util.ListenableFutureTask;
import org.whispersystems.textsecure.storage.CanonicalRecipientAddress;
import java.util.HashSet; import java.util.HashSet;
public class Recipient implements Parcelable, CanonicalRecipientAddress { public class Recipient implements Parcelable, CanonicalRecipient {
public static final Parcelable.Creator<Recipient> CREATOR = new Parcelable.Creator<Recipient>() { public static final Parcelable.Creator<Recipient> CREATOR = new Parcelable.Creator<Recipient>() {
public Recipient createFromParcel(Parcel in) { public Recipient createFromParcel(Parcel in) {
@ -43,18 +44,21 @@ public class Recipient implements Parcelable, CanonicalRecipientAddress {
} }
}; };
private final String number;
private final HashSet<RecipientModifiedListener> listeners = new HashSet<RecipientModifiedListener>(); private final HashSet<RecipientModifiedListener> listeners = new HashSet<RecipientModifiedListener>();
private final String number;
private final long recipientId;
private String name; private String name;
private Bitmap contactPhoto; private Bitmap contactPhoto;
private Uri contactUri; private Uri contactUri;
public Recipient(String number, Bitmap contactPhoto, Recipient(String number, Bitmap contactPhoto, long recipientId,
ListenableFutureTask<RecipientDetails> future) ListenableFutureTask<RecipientDetails> future)
{ {
this.number = number; this.number = number;
this.contactPhoto = contactPhoto; this.contactPhoto = contactPhoto;
this.recipientId = recipientId;
future.setListener(new FutureTaskListener<RecipientDetails>() { future.setListener(new FutureTaskListener<RecipientDetails>() {
@Override @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.number = number;
this.recipientId = recipientId;
this.contactUri = contactUri; this.contactUri = contactUri;
this.name = name; this.name = name;
this.contactPhoto = contactPhoto; this.contactPhoto = contactPhoto;
@ -92,6 +97,7 @@ public class Recipient implements Parcelable, CanonicalRecipientAddress {
public Recipient(Parcel in) { public Recipient(Parcel in) {
this.number = in.readString(); this.number = in.readString();
this.name = in.readString(); this.name = in.readString();
this.recipientId = in.readLong();
this.contactUri = (Uri)in.readParcelable(null); this.contactUri = (Uri)in.readParcelable(null);
this.contactPhoto = (Bitmap)in.readParcelable(null); this.contactPhoto = (Bitmap)in.readParcelable(null);
} }
@ -112,6 +118,14 @@ public class Recipient implements Parcelable, CanonicalRecipientAddress {
return 0; return 0;
} }
public long getRecipientId() {
return recipientId;
}
public boolean isGroupRecipient() {
return GroupUtil.isEncodedGroup(number);
}
// public void updateAsynchronousContent(RecipientDetails result) { // public void updateAsynchronousContent(RecipientDetails result) {
// if (result != null) { // if (result != null) {
// Recipient.this.name.set(result.name); // Recipient.this.name.set(result.name);
@ -136,6 +150,7 @@ public class Recipient implements Parcelable, CanonicalRecipientAddress {
public synchronized void writeToParcel(Parcel dest, int flags) { public synchronized void writeToParcel(Parcel dest, int flags) {
dest.writeString(number); dest.writeString(number);
dest.writeString(name); dest.writeString(name);
dest.writeLong(recipientId);
dest.writeParcelable(contactUri, 0); dest.writeParcelable(contactUri, 0);
dest.writeParcelable(contactPhoto, 0); dest.writeParcelable(contactPhoto, 0);
} }
@ -148,11 +163,12 @@ public class Recipient implements Parcelable, CanonicalRecipientAddress {
return contactPhoto; return contactPhoto;
} }
public long getCanonicalAddress(Context context) { public static Recipient getUnknownRecipient(Context context) {
return CanonicalAddressDatabase.getInstance(context).getCanonicalAddress(getNumber()); return new Recipient("Unknown", "Unknown", -1, null, ContactPhotoFactory.getDefaultContactPhoto(context));
} }
public static interface RecipientModifiedListener { public static interface RecipientModifiedListener {
public void onModified(Recipient recipient); public void onModified(Recipient recipient);
} }
} }

View File

@ -20,6 +20,7 @@ import android.content.Context;
import android.util.Log; import android.util.Log;
import org.thoughtcrime.securesms.contacts.ContactPhotoFactory; import org.thoughtcrime.securesms.contacts.ContactPhotoFactory;
import org.thoughtcrime.securesms.database.CanonicalAddressDatabase;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.util.NumberUtil; import org.thoughtcrime.securesms.util.NumberUtil;
import org.whispersystems.textsecure.push.IncomingPushMessage; import org.whispersystems.textsecure.push.IncomingPushMessage;
@ -51,7 +52,8 @@ public class RecipientFactory {
} }
private static Recipient getRecipientForNumber(Context context, String number, boolean asynchronous) { 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) public static Recipients getRecipientsFromString(Context context, String rawText, boolean asynchronous)
@ -81,17 +83,16 @@ public class RecipientFactory {
return getRecipientsFromString(context, message.getSource(), asynchronous); return getRecipientsFromString(context, message.getSource(), asynchronous);
} catch (RecipientFormattingException e) { } catch (RecipientFormattingException e) {
Log.w("RecipientFactory", e); Log.w("RecipientFactory", e);
return new Recipients(new Recipient("Unknown", "Unknown", null, return new Recipients(Recipient.getUnknownRecipient(context));
ContactPhotoFactory.getDefaultContactPhoto(context)));
} }
} }
private static Recipient getRecipientFromProviderId(Context context, String recipientId, boolean asynchronous) { private static Recipient getRecipientFromProviderId(Context context, String recipientId, boolean asynchronous) {
if (recipientId.startsWith("g_")) { try {
return provider.getGroupRecipient(context, recipientId, asynchronous); return provider.getRecipient(context, Long.parseLong(recipientId), asynchronous);
} else { } catch (NumberFormatException e) {
String number = DatabaseFactory.getAddressDatabase(context).getAddressFromId(recipientId); Log.w("RecipientFactory", e);
return getRecipientForNumber(context, number, asynchronous); return Recipient.getUnknownRecipient(context);
} }
} }
@ -126,7 +127,7 @@ public class RecipientFactory {
if (hasBracketedNumber(recipient)) if (hasBracketedNumber(recipient))
return getRecipientForNumber(context, parseBracketedNumber(recipient), asynchronous); return getRecipientForNumber(context, parseBracketedNumber(recipient), asynchronous);
if (NumberUtil.isValidSmsOrEmail(recipient)) if (NumberUtil.isValidSmsOrEmailOrGroup(recipient))
return getRecipientForNumber(context, recipient, asynchronous); return getRecipientForNumber(context, recipient, asynchronous);
throw new RecipientFormattingException("Recipient: " + recipient + " is badly formatted."); throw new RecipientFormattingException("Recipient: " + recipient + " is badly formatted.");

View File

@ -27,12 +27,15 @@ import android.provider.ContactsContract.PhoneLookup;
import android.util.Log; import android.util.Log;
import org.thoughtcrime.securesms.contacts.ContactPhotoFactory; import org.thoughtcrime.securesms.contacts.ContactPhotoFactory;
import org.thoughtcrime.securesms.database.CanonicalAddressDatabase;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.LRUCache; import org.thoughtcrime.securesms.util.LRUCache;
import org.whispersystems.textsecure.util.ListenableFutureTask; import org.whispersystems.textsecure.util.ListenableFutureTask;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.Collections; import java.util.Collections;
import java.util.Map; import java.util.Map;
@ -41,7 +44,7 @@ import java.util.concurrent.ExecutorService;
public class RecipientProvider { public class RecipientProvider {
private static final Map<String,Recipient> recipientCache = Collections.synchronizedMap(new LRUCache<String,Recipient>(1000)); private static final Map<Long,Recipient> recipientCache = Collections.synchronizedMap(new LRUCache<Long,Recipient>(1000));
private static final ExecutorService asyncRecipientResolver = Util.newSingleThreadedLifoExecutor(); private static final ExecutorService asyncRecipientResolver = Util.newSingleThreadedLifoExecutor();
private static final String[] CALLER_ID_PROJECTION = new String[] { private static final String[] CALLER_ID_PROJECTION = new String[] {
@ -50,58 +53,45 @@ public class RecipientProvider {
PhoneLookup._ID, PhoneLookup._ID,
}; };
public Recipient getRecipient(Context context, String number, boolean asynchronous) { public Recipient getRecipient(Context context, long recipientId, boolean asynchronous) {
Recipient cachedRecipient = recipientCache.get(number); Recipient cachedRecipient = recipientCache.get(recipientId);
if (cachedRecipient != null) return cachedRecipient; if (cachedRecipient != null) return cachedRecipient;
else if (asynchronous) return getAsynchronousRecipient(context, number); else if (asynchronous) return getAsynchronousRecipient(context, recipientId);
else return getSynchronousRecipient(context, number); else return getSynchronousRecipient(context, recipientId);
} }
public Recipient getGroupRecipient(Context context, String groupId, boolean asynchronous) { private Recipient getSynchronousRecipient(Context context, long recipientId) {
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) {
Log.w("RecipientProvider", "Cache miss [SYNC]!"); Log.w("RecipientProvider", "Cache miss [SYNC]!");
RecipientDetails details = getRecipientDetails(context, number);
Recipient recipient; 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) { 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 { } 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; return recipient;
} }
private Recipient getSynchronousGroupRecipient(Context context, String groupId) { private Recipient getAsynchronousRecipient(final Context context, final long recipientId) {
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) {
Log.w("RecipientProvider", "Cache miss [ASYNC]!"); Log.w("RecipientProvider", "Cache miss [ASYNC]!");
final String number = CanonicalAddressDatabase.getInstance(context).getAddressFromId(String.valueOf(recipientId));
Callable<RecipientDetails> task = new Callable<RecipientDetails>() { Callable<RecipientDetails> task = new Callable<RecipientDetails>() {
@Override @Override
public RecipientDetails call() throws Exception { 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); asyncRecipientResolver.submit(future);
Recipient recipient = new Recipient(number, ContactPhotoFactory.getDefaultContactPhoto(context), future); Recipient recipient = new Recipient(number, ContactPhotoFactory.getDefaultContactPhoto(context), recipientId, future);
recipientCache.put(number, recipient); recipientCache.put(recipientId, recipient);
return recipient;
}
private Recipient getAsynchronousGroupRecipient(final Context context, final String groupId) {
Callable<RecipientDetails> task = new Callable<RecipientDetails>() {
@Override
public RecipientDetails call() throws Exception {
return getGroupRecipientDetails(context, groupId);
}
};
ListenableFutureTask<RecipientDetails> future = new ListenableFutureTask<RecipientDetails>(task, null);
asyncRecipientResolver.submit(future);
Recipient recipient = new Recipient(groupId, ContactPhotoFactory.getDefaultContactPhoto(context), future);
recipientCache.put(groupId, recipient);
return recipient; return recipient;
} }
@ -159,7 +131,10 @@ public class RecipientProvider {
} }
private RecipientDetails getGroupRecipientDetails(Context context, String groupId) { private RecipientDetails getGroupRecipientDetails(Context context, String groupId) {
GroupDatabase.Reader reader = DatabaseFactory.getGroupDatabase(context).getGroup(groupId.substring(2)); try {
GroupDatabase.Reader reader = DatabaseFactory.getGroupDatabase(context)
.getGroup(GroupUtil.getDecodedId(groupId));
GroupDatabase.GroupRecord record; GroupDatabase.GroupRecord record;
try { try {
@ -177,6 +152,10 @@ public class RecipientProvider {
} }
return null; return null;
} catch (IOException e) {
Log.w("RecipientProvider", e);
return null;
}
} }
private Bitmap getContactPhoto(Context context, Uri uri) { private Bitmap getContactPhoto(Context context, Uri uri) {

View File

@ -21,6 +21,7 @@ import android.os.Parcelable;
import android.util.Patterns; import android.util.Patterns;
import org.thoughtcrime.securesms.recipients.Recipient.RecipientModifiedListener; import org.thoughtcrime.securesms.recipients.Recipient.RecipientModifiedListener;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.NumberUtil; import org.thoughtcrime.securesms.util.NumberUtil;
import java.util.ArrayList; import java.util.ArrayList;
@ -89,6 +90,10 @@ public class Recipients implements Parcelable {
return false; return false;
} }
public boolean isGroupRecipient() {
return isSingleRecipient() && GroupUtil.isEncodedGroup(recipients.get(0).getNumber());
}
// public Recipients getSecureSessionRecipients(Context context) { // public Recipients getSecureSessionRecipients(Context context) {
// List<Recipient> secureRecipients = new LinkedList<Recipient>(); // List<Recipient> secureRecipients = new LinkedList<Recipient>();
// //

View File

@ -33,7 +33,7 @@ public class AvatarDownloader {
if (!SendReceiveService.DOWNLOAD_AVATAR_ACTION.equals(intent.getAction())) if (!SendReceiveService.DOWNLOAD_AVATAR_ACTION.equals(intent.getAction()))
return; return;
String groupId = intent.getStringExtra("group_id"); byte[] groupId = intent.getByteArrayExtra("group_id");
GroupDatabase database = DatabaseFactory.getGroupDatabase(context); GroupDatabase database = DatabaseFactory.getGroupDatabase(context);
GroupDatabase.Reader reader = database.getGroup(groupId); GroupDatabase.Reader reader = database.getGroup(groupId);

View File

@ -17,12 +17,14 @@ import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory; import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.recipients.Recipients; import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.sms.IncomingEncryptedMessage; import org.thoughtcrime.securesms.sms.IncomingEncryptedMessage;
import org.thoughtcrime.securesms.sms.IncomingKeyExchangeMessage; import org.thoughtcrime.securesms.sms.IncomingKeyExchangeMessage;
import org.thoughtcrime.securesms.sms.IncomingPreKeyBundleMessage; import org.thoughtcrime.securesms.sms.IncomingPreKeyBundleMessage;
import org.thoughtcrime.securesms.sms.IncomingTextMessage; import org.thoughtcrime.securesms.sms.IncomingTextMessage;
import org.thoughtcrime.securesms.sms.SmsTransportDetails; import org.thoughtcrime.securesms.sms.SmsTransportDetails;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.textsecure.crypto.InvalidKeyException; import org.whispersystems.textsecure.crypto.InvalidKeyException;
import org.whispersystems.textsecure.crypto.InvalidMessageException; 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.IncomingPushMessage;
import org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent; import org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent;
import org.whispersystems.textsecure.storage.InvalidKeyIdException; import org.whispersystems.textsecure.storage.InvalidKeyIdException;
import org.whispersystems.textsecure.storage.RecipientDevice;
import org.whispersystems.textsecure.util.Hex; import org.whispersystems.textsecure.util.Hex;
import ws.com.google.android.mms.MmsException; import ws.com.google.android.mms.MmsException;
@ -106,8 +109,9 @@ public class PushReceiver {
} }
try { try {
Recipient recipient = new Recipient(null, message.getSource(), null, null); Recipient recipient = RecipientFactory.getRecipientsFromString(context, message.getSource(), false).getPrimaryRecipient();
KeyExchangeProcessorV2 processor = new KeyExchangeProcessorV2(context, masterSecret, recipient); RecipientDevice recipientDevice = new RecipientDevice(recipient.getRecipientId(), message.getSourceDevice());
KeyExchangeProcessorV2 processor = new KeyExchangeProcessorV2(context, masterSecret, recipientDevice);
PreKeyWhisperMessage preKeyExchange = new PreKeyWhisperMessage(message.getBody()); PreKeyWhisperMessage preKeyExchange = new PreKeyWhisperMessage(message.getBody());
if (processor.isTrusted(preKeyExchange)) { if (processor.isTrusted(preKeyExchange)) {
@ -135,6 +139,9 @@ public class PushReceiver {
} catch (InvalidMessageException e) { } catch (InvalidMessageException e) {
Log.w("PushReceiver", e); Log.w("PushReceiver", e);
handleReceivedCorruptedKey(masterSecret, message, false); 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); handleReceivedGroupMessage(masterSecret, message, messageContent, secure);
} else if (messageContent.getAttachmentsCount() > 0) { } else if (messageContent.getAttachmentsCount() > 0) {
Log.w("PushReceiver", "Received push media message..."); Log.w("PushReceiver", "Received push media message...");
handleReceivedMediaMessage(masterSecret, message, messageContent, secure, null); handleReceivedMediaMessage(masterSecret, message, messageContent, secure);
} else { } else {
Log.w("PushReceiver", "Received push text message..."); Log.w("PushReceiver", "Received push text message...");
handleReceivedTextMessage(masterSecret, message, messageContent, secure, null); handleReceivedTextMessage(masterSecret, message, messageContent, secure);
} }
} catch (InvalidProtocolBufferException e) { } catch (InvalidProtocolBufferException e) {
Log.w("PushReceiver", e); Log.w("PushReceiver", e);
@ -207,30 +214,28 @@ public class PushReceiver {
if (group.hasAvatar()) { if (group.hasAvatar()) {
Intent intent = new Intent(context, SendReceiveService.class); Intent intent = new Intent(context, SendReceiveService.class);
intent.setAction(SendReceiveService.DOWNLOAD_AVATAR_ACTION); intent.setAction(SendReceiveService.DOWNLOAD_AVATAR_ACTION);
intent.putExtra("group_id", group.getId().toByteArray());
context.startService(intent); context.startService(intent);
} }
String groupId = "g_" + Hex.toString(group.getId().toByteArray());
if (messageContent.getAttachmentsCount() > 0) { if (messageContent.getAttachmentsCount() > 0) {
handleReceivedMediaMessage(masterSecret, message, messageContent, secure, groupId); handleReceivedMediaMessage(masterSecret, message, messageContent, secure);
} else if (messageContent.hasBody()) { } else if (messageContent.hasBody()) {
handleReceivedTextMessage(masterSecret, message, messageContent, secure, groupId); handleReceivedTextMessage(masterSecret, message, messageContent, secure);
} }
} }
private void handleReceivedMediaMessage(MasterSecret masterSecret, private void handleReceivedMediaMessage(MasterSecret masterSecret,
IncomingPushMessage message, IncomingPushMessage message,
PushMessageContent messageContent, PushMessageContent messageContent,
boolean secure, String groupId) boolean secure)
{ {
try { try {
String localNumber = TextSecurePreferences.getLocalNumber(context); String localNumber = TextSecurePreferences.getLocalNumber(context);
MmsDatabase database = DatabaseFactory.getMmsDatabase(context); MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
IncomingMediaMessage mediaMessage = new IncomingMediaMessage(masterSecret, localNumber, IncomingMediaMessage mediaMessage = new IncomingMediaMessage(masterSecret, localNumber,
message, messageContent, message, messageContent);
groupId);
Pair<Long, Long> messageAndThreadId; Pair<Long, Long> messageAndThreadId;
@ -255,9 +260,10 @@ public class PushReceiver {
private void handleReceivedTextMessage(MasterSecret masterSecret, private void handleReceivedTextMessage(MasterSecret masterSecret,
IncomingPushMessage message, IncomingPushMessage message,
PushMessageContent messageContent, PushMessageContent messageContent,
boolean secure, String groupId) boolean secure)
{ {
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context); EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
String groupId = messageContent.hasGroup() ? GroupUtil.getEncodedId(messageContent.getGroup().getId().toByteArray()) : null;
IncomingTextMessage textMessage = new IncomingTextMessage(message, "", groupId); IncomingTextMessage textMessage = new IncomingTextMessage(message, "", groupId);
if (secure) { if (secure) {

View File

@ -32,6 +32,8 @@ import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.protocol.WirePrefix; import org.thoughtcrime.securesms.protocol.WirePrefix;
import org.thoughtcrime.securesms.recipients.Recipient; 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.IncomingEncryptedMessage;
import org.thoughtcrime.securesms.sms.IncomingKeyExchangeMessage; import org.thoughtcrime.securesms.sms.IncomingKeyExchangeMessage;
import org.thoughtcrime.securesms.sms.IncomingPreKeyBundleMessage; 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.PreKeyWhisperMessage;
import org.whispersystems.textsecure.crypto.protocol.WhisperMessageV2; import org.whispersystems.textsecure.crypto.protocol.WhisperMessageV2;
import org.whispersystems.textsecure.storage.InvalidKeyIdException; import org.whispersystems.textsecure.storage.InvalidKeyIdException;
import org.whispersystems.textsecure.storage.RecipientDevice;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
@ -80,8 +83,9 @@ public class SmsReceiver {
if (masterSecret != null) { if (masterSecret != null) {
DecryptingQueue.scheduleDecryption(context, masterSecret, messageAndThreadId.first, DecryptingQueue.scheduleDecryption(context, masterSecret, messageAndThreadId.first,
messageAndThreadId.second, messageAndThreadId.second,
message.getSender(), message.getMessageBody(), message.getSender(), message.getSenderDeviceId(),
message.isSecureMessage(), message.isKeyExchange()); message.getMessageBody(), message.isSecureMessage(),
message.isKeyExchange());
} }
return messageAndThreadId; return messageAndThreadId;
@ -106,8 +110,9 @@ public class SmsReceiver {
Log.w("SmsReceiver", "Processing prekey message..."); Log.w("SmsReceiver", "Processing prekey message...");
try { try {
Recipient recipient = new Recipient(null, message.getSender(), null, null); Recipient recipient = RecipientFactory.getRecipientsFromString(context, message.getSender(), false).getPrimaryRecipient();
KeyExchangeProcessorV2 processor = new KeyExchangeProcessorV2(context, masterSecret, recipient); RecipientDevice recipientDevice = new RecipientDevice(recipient.getRecipientId(), message.getSenderDeviceId());
KeyExchangeProcessorV2 processor = new KeyExchangeProcessorV2(context, masterSecret, recipientDevice);
SmsTransportDetails transportDetails = new SmsTransportDetails(); SmsTransportDetails transportDetails = new SmsTransportDetails();
byte[] rawMessage = transportDetails.getDecodedMessage(message.getMessageBody().getBytes()); byte[] rawMessage = transportDetails.getDecodedMessage(message.getMessageBody().getBytes());
PreKeyWhisperMessage preKeyExchange = new PreKeyWhisperMessage(rawMessage); PreKeyWhisperMessage preKeyExchange = new PreKeyWhisperMessage(rawMessage);
@ -142,6 +147,9 @@ public class SmsReceiver {
} catch (InvalidMessageException e) { } catch (InvalidMessageException e) {
Log.w("SmsReceiver", e); Log.w("SmsReceiver", e);
message.setCorrupted(true); message.setCorrupted(true);
} catch (RecipientFormattingException e) {
Log.w("SmsReceiver", e);
message.setCorrupted(true);
} }
return storeStandardMessage(masterSecret, message); return storeStandardMessage(masterSecret, message);
@ -152,9 +160,10 @@ public class SmsReceiver {
{ {
if (masterSecret != null && TextSecurePreferences.isAutoRespondKeyExchangeEnabled(context)) { if (masterSecret != null && TextSecurePreferences.isAutoRespondKeyExchangeEnabled(context)) {
try { 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()); 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)) { if (processor.isStale(exchangeMessage)) {
message.setStale(true); message.setStale(true);
@ -175,6 +184,9 @@ public class SmsReceiver {
} catch (InvalidMessageException e) { } catch (InvalidMessageException e) {
Log.w("SmsReceiver", e); Log.w("SmsReceiver", e);
message.setCorrupted(true); message.setCorrupted(true);
} catch (RecipientFormattingException e) {
Log.w("SmsReceiver", e);
message.setCorrupted(true);
} }
} }

View File

@ -46,11 +46,11 @@ public class SmsSender {
} }
public void process(MasterSecret masterSecret, Intent intent) { 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); handleSendMessage(masterSecret, intent);
} else if (intent.getAction().equals(SendReceiveService.SENT_SMS_ACTION)) { } else if (SendReceiveService.SENT_SMS_ACTION.equals(intent.getAction())) {
handleSentMessage(intent); handleSentMessage(intent);
} else if (intent.getAction().equals(SendReceiveService.DELIVERED_SMS_ACTION)) { } else if (SendReceiveService.DELIVERED_SMS_ACTION.equals(intent.getAction())) {
handleDeliveredMessage(intent); handleDeliveredMessage(intent);
} }
} }

View File

@ -5,6 +5,7 @@ import android.os.Parcelable;
import android.telephony.SmsMessage; import android.telephony.SmsMessage;
import org.whispersystems.textsecure.push.IncomingPushMessage; import org.whispersystems.textsecure.push.IncomingPushMessage;
import org.whispersystems.textsecure.storage.RecipientDevice;
import java.util.List; import java.util.List;
@ -24,6 +25,7 @@ public class IncomingTextMessage implements Parcelable {
private final String message; private final String message;
private final String sender; private final String sender;
private final int senderDeviceId;
private final int protocol; private final int protocol;
private final String serviceCenterAddress; private final String serviceCenterAddress;
private final boolean replyPathPresent; private final boolean replyPathPresent;
@ -34,6 +36,7 @@ public class IncomingTextMessage implements Parcelable {
public IncomingTextMessage(SmsMessage message) { public IncomingTextMessage(SmsMessage message) {
this.message = message.getDisplayMessageBody(); this.message = message.getDisplayMessageBody();
this.sender = message.getDisplayOriginatingAddress(); this.sender = message.getDisplayOriginatingAddress();
this.senderDeviceId = RecipientDevice.DEFAULT_DEVICE_ID;
this.protocol = message.getProtocolIdentifier(); this.protocol = message.getProtocolIdentifier();
this.serviceCenterAddress = message.getServiceCenterAddress(); this.serviceCenterAddress = message.getServiceCenterAddress();
this.replyPathPresent = message.isReplyPathPresent(); this.replyPathPresent = message.isReplyPathPresent();
@ -45,6 +48,7 @@ public class IncomingTextMessage implements Parcelable {
public IncomingTextMessage(IncomingPushMessage message, String encodedBody, String groupId) { public IncomingTextMessage(IncomingPushMessage message, String encodedBody, String groupId) {
this.message = encodedBody; this.message = encodedBody;
this.sender = message.getSource(); this.sender = message.getSource();
this.senderDeviceId = message.getSourceDevice();
this.protocol = 31337; this.protocol = 31337;
this.serviceCenterAddress = "GCM"; this.serviceCenterAddress = "GCM";
this.replyPathPresent = true; this.replyPathPresent = true;
@ -56,6 +60,7 @@ public class IncomingTextMessage implements Parcelable {
public IncomingTextMessage(Parcel in) { public IncomingTextMessage(Parcel in) {
this.message = in.readString(); this.message = in.readString();
this.sender = in.readString(); this.sender = in.readString();
this.senderDeviceId = in.readInt();
this.protocol = in.readInt(); this.protocol = in.readInt();
this.serviceCenterAddress = in.readString(); this.serviceCenterAddress = in.readString();
this.replyPathPresent = (in.readInt() == 1); this.replyPathPresent = (in.readInt() == 1);
@ -67,6 +72,7 @@ public class IncomingTextMessage implements Parcelable {
public IncomingTextMessage(IncomingTextMessage base, String newBody) { public IncomingTextMessage(IncomingTextMessage base, String newBody) {
this.message = newBody; this.message = newBody;
this.sender = base.getSender(); this.sender = base.getSender();
this.senderDeviceId = base.getSenderDeviceId();
this.protocol = base.getProtocol(); this.protocol = base.getProtocol();
this.serviceCenterAddress = base.getServiceCenterAddress(); this.serviceCenterAddress = base.getServiceCenterAddress();
this.replyPathPresent = base.isReplyPathPresent(); this.replyPathPresent = base.isReplyPathPresent();
@ -84,6 +90,7 @@ public class IncomingTextMessage implements Parcelable {
this.message = body.toString(); this.message = body.toString();
this.sender = fragments.get(0).getSender(); this.sender = fragments.get(0).getSender();
this.senderDeviceId = fragments.get(0).getSenderDeviceId();
this.protocol = fragments.get(0).getProtocol(); this.protocol = fragments.get(0).getProtocol();
this.serviceCenterAddress = fragments.get(0).getServiceCenterAddress(); this.serviceCenterAddress = fragments.get(0).getServiceCenterAddress();
this.replyPathPresent = fragments.get(0).isReplyPathPresent(); this.replyPathPresent = fragments.get(0).isReplyPathPresent();
@ -112,6 +119,10 @@ public class IncomingTextMessage implements Parcelable {
return sender; return sender;
} }
public int getSenderDeviceId() {
return senderDeviceId;
}
public int getProtocol() { public int getProtocol() {
return protocol; return protocol;
} }
@ -149,6 +160,7 @@ public class IncomingTextMessage implements Parcelable {
public void writeToParcel(Parcel out, int flags) { public void writeToParcel(Parcel out, int flags) {
out.writeString(message); out.writeString(message);
out.writeString(sender); out.writeString(sender);
out.writeInt(senderDeviceId);
out.writeInt(protocol); out.writeInt(protocol);
out.writeString(serviceCenterAddress); out.writeString(serviceCenterAddress);
out.writeInt(replyPathPresent ? 1 : 0); out.writeInt(replyPathPresent ? 1 : 0);

View File

@ -29,9 +29,12 @@ import org.thoughtcrime.securesms.mms.MmsSendResult;
import org.thoughtcrime.securesms.mms.TextTransport; import org.thoughtcrime.securesms.mms.TextTransport;
import org.thoughtcrime.securesms.protocol.WirePrefix; import org.thoughtcrime.securesms.protocol.WirePrefix;
import org.thoughtcrime.securesms.recipients.Recipient; 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.MasterSecret;
import org.whispersystems.textsecure.crypto.SessionCipher; import org.whispersystems.textsecure.crypto.SessionCipher;
import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage; import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage;
import org.whispersystems.textsecure.storage.RecipientDevice;
import org.whispersystems.textsecure.util.Hex; import org.whispersystems.textsecure.util.Hex;
import java.io.IOException; import java.io.IOException;
@ -153,12 +156,18 @@ public class MmsTransport {
} }
private byte[] getEncryptedPdu(MasterSecret masterSecret, String recipientString, byte[] pduBytes) { private byte[] getEncryptedPdu(MasterSecret masterSecret, String recipientString, byte[] pduBytes) {
try {
TextTransport transportDetails = new TextTransport(); TextTransport transportDetails = new TextTransport();
Recipient recipient = new Recipient(null, recipientString, null, null); Recipient recipient = RecipientFactory.getRecipientsFromString(context, recipientString, false).getPrimaryRecipient();
SessionCipher sessionCipher = SessionCipher.createFor(context, masterSecret, recipient); RecipientDevice recipientDevice = new RecipientDevice(recipient.getRecipientId(), RecipientDevice.DEFAULT_DEVICE_ID);
SessionCipher sessionCipher = SessionCipher.createFor(context, masterSecret, recipientDevice);
CiphertextMessage ciphertextMessage = sessionCipher.encrypt(pduBytes); 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) { private boolean isInconsistentResponse(SendReq message, SendConf response) {

View File

@ -23,6 +23,7 @@ import android.util.Log;
import com.google.protobuf.ByteString; import com.google.protobuf.ByteString;
import org.thoughtcrime.securesms.crypto.KeyExchangeProcessorV2; import org.thoughtcrime.securesms.crypto.KeyExchangeProcessorV2;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord; import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.mms.PartParser; import org.thoughtcrime.securesms.mms.PartParser;
import org.thoughtcrime.securesms.push.PushServiceSocketFactory; 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.RecipientFactory;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException; import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.recipients.Recipients; 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.thoughtcrime.securesms.util.Util;
import org.whispersystems.textsecure.crypto.AttachmentCipher; import org.whispersystems.textsecure.crypto.AttachmentCipher;
import org.whispersystems.textsecure.crypto.InvalidKeyException; import org.whispersystems.textsecure.crypto.InvalidKeyException;
import org.whispersystems.textsecure.crypto.MasterSecret; import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.crypto.SessionCipher; import org.whispersystems.textsecure.crypto.SessionCipher;
import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage; 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.OutgoingPushMessage;
import org.whispersystems.textsecure.push.OutgoingPushMessageList;
import org.whispersystems.textsecure.push.PreKeyEntity; import org.whispersystems.textsecure.push.PreKeyEntity;
import org.whispersystems.textsecure.push.PushAddress;
import org.whispersystems.textsecure.push.PushAttachmentData; import org.whispersystems.textsecure.push.PushAttachmentData;
import org.whispersystems.textsecure.push.PushAttachmentPointer; import org.whispersystems.textsecure.push.PushAttachmentPointer;
import org.whispersystems.textsecure.push.PushBody; import org.whispersystems.textsecure.push.PushBody;
import org.whispersystems.textsecure.push.PushDestination;
import org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent; import org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent;
import org.whispersystems.textsecure.push.PushServiceSocket; import org.whispersystems.textsecure.push.PushServiceSocket;
import org.whispersystems.textsecure.push.RateLimitException;
import org.whispersystems.textsecure.push.UnregisteredUserException; import org.whispersystems.textsecure.push.UnregisteredUserException;
import org.whispersystems.textsecure.storage.SessionRecordV2; import org.whispersystems.textsecure.storage.SessionRecordV2;
import org.whispersystems.textsecure.util.InvalidNumberException; import org.whispersystems.textsecure.util.InvalidNumberException;
@ -70,79 +73,70 @@ public class PushTransport extends BaseTransport {
public void deliver(SmsMessageRecord message) throws IOException { public void deliver(SmsMessageRecord message) throws IOException {
try { try {
String localNumber = TextSecurePreferences.getLocalNumber(context);
Recipient recipient = message.getIndividualRecipient(); Recipient recipient = message.getIndividualRecipient();
long threadId = message.getThreadId(); long threadId = message.getThreadId();
PushServiceSocket socket = PushServiceSocketFactory.create(context); PushServiceSocket socket = PushServiceSocketFactory.create(context);
PushDestination destination = PushDestination.create(context, localNumber, byte[] plaintext = PushMessageContent.newBuilder()
recipient.getNumber()); .setBody(message.getBody().getBody())
.build().toByteArray();
String plaintextBody = message.getBody().getBody(); deliver(socket, recipient, threadId, plaintext);
byte[] plaintext = PushMessageContent.newBuilder().setBody(plaintextBody).build().toByteArray();
PushBody pushBody = getEncryptedMessage(socket, threadId, recipient, destination, plaintext);
socket.sendMessage(destination, pushBody);
context.sendBroadcast(constructSentIntent(context, message.getId(), message.getType(), true)); context.sendBroadcast(constructSentIntent(context, message.getId(), message.getType(), true));
} catch (UnregisteredUserException e) { } catch (UnregisteredUserException e) {
Log.w("PushTransport", 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."); throw new IOException("Not push registered after all.");
} catch (RateLimitException e) {
Log.w("PushTransport", e);
throw new IOException("Rate limit exceeded.");
} catch (InvalidNumberException e) { } catch (InvalidNumberException e) {
Log.w("PushTransport", e); Log.w("PushTransport", e);
throw new IOException("Badly formatted number."); throw new IOException("Badly formatted number.");
} }
} }
public void deliver(SendReq message, List<PushDestination> destinations, long threadId) public void deliver(SendReq message, long threadId) throws IOException {
throws IOException
{
try {
PushServiceSocket socket = PushServiceSocketFactory.create(context); PushServiceSocket socket = PushServiceSocketFactory.create(context);
String messageBody = PartParser.getMessageText(message.getBody()); byte[] plaintext = getPlaintextMessage(socket, message);
List<PushBody> pushBodies = new LinkedList<PushBody>(); String destination = message.getTo()[0].getString();
for (PushDestination destination : destinations) { Recipients recipients;
Recipients recipients = RecipientFactory.getRecipientsFromString(context, destination.getNumber(), false);
List<PushAttachmentPointer> attachments = getPushAttachmentPointers(socket, message.getBody());
PushMessageContent.Builder builder = PushMessageContent.newBuilder();
if (messageBody != null) { try {
builder.setBody(messageBody); if (GroupUtil.isEncodedGroup(destination)) {
recipients = DatabaseFactory.getGroupDatabase(context)
.getGroupMembers(GroupUtil.getDecodedId(destination));
} else {
recipients = RecipientFactory.getRecipientsFromString(context, destination, false);
} }
for (PushAttachmentPointer attachment : attachments) { for (Recipient recipient : recipients.getRecipientsList()) {
PushMessageContent.AttachmentPointer.Builder attachmentBuilder = deliver(socket, recipient, threadId, plaintext);
PushMessageContent.AttachmentPointer.newBuilder();
attachmentBuilder.setId(attachment.getId());
attachmentBuilder.setContentType(attachment.getContentType());
attachmentBuilder.setKey(ByteString.copyFrom(attachment.getKey()));
builder.addAttachments(attachmentBuilder.build());
} }
} catch (UnregisteredUserException uue) {
byte[] plaintext = builder.build().toByteArray(); // TODO: We should probably remove the user from the directory?
PushBody pushBody = getEncryptedMessage(socket, threadId, recipients.getPrimaryRecipient(), destination, plaintext); throw new IOException(uue);
pushBodies.add(pushBody);
}
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.");
} catch (RecipientFormattingException e) { } catch (RecipientFormattingException e) {
Log.w("PushTransport", e); throw new IOException(e);
throw new IOException("Bad destination!"); } 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; return attachments;
} }
private PushBody getEncryptedMessage(PushServiceSocket socket, long threadId, Recipient recipient, private void handleMismatchedDevices(PushServiceSocket socket, long threadId,
PushDestination pushDestination, byte[] plaintext) Recipient recipient,
throws IOException MismatchedDevices mismatchedDevices)
throws InvalidNumberException, IOException
{ {
if (!SessionRecordV2.hasSession(context, masterSecret, recipient)) {
try { try {
PreKeyEntity preKey = socket.getPreKey(pushDestination); String e164number = Util.canonicalizeNumber(context, recipient.getNumber());
KeyExchangeProcessorV2 processor = new KeyExchangeProcessorV2(context, masterSecret, recipient); 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); processor.processKeyExchangeMessage(preKey, threadId);
}
} catch (InvalidKeyException e) { } catch (InvalidKeyException e) {
Log.w("PushTransport", e); throw new IOException(e);
throw new IOException("Invalid PreKey!");
} }
} }
SessionCipher cipher = SessionCipher.createFor(context, masterSecret, recipient); private byte[] getPlaintextMessage(PushServiceSocket socket, SendReq message) throws IOException {
String messageBody = PartParser.getMessageText(message.getBody());
List<PushAttachmentPointer> 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<OutgoingPushMessage> messages = new LinkedList<OutgoingPushMessage>();
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<PreKeyEntity> 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) {
throw new IOException(e);
}
}
SessionCipher cipher = SessionCipher.createFor(context, masterSecret, pushAddress);
CiphertextMessage message = cipher.encrypt(plaintext); CiphertextMessage message = cipher.encrypt(plaintext);
if (message.getType() == CiphertextMessage.PREKEY_WHISPER_TYPE) { if (message.getType() == CiphertextMessage.PREKEY_WHISPER_TYPE) {
@ -198,15 +277,7 @@ public class PushTransport extends BaseTransport {
} }
} }
private void destroySessions(List<String> unregisteredUsers) { private void destroySessions(Recipient recipient) {
for (String unregisteredUser : unregisteredUsers) { SessionRecordV2.deleteAll(context, recipient);
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);
}
}
} }
} }

View File

@ -34,6 +34,7 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.textsecure.crypto.MasterSecret; import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.crypto.SessionCipher; import org.whispersystems.textsecure.crypto.SessionCipher;
import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage; import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage;
import org.whispersystems.textsecure.storage.RecipientDevice;
import java.util.ArrayList; import java.util.ArrayList;
@ -160,9 +161,10 @@ public class SmsTransport extends BaseTransport {
OutgoingTextMessage message) OutgoingTextMessage message)
{ {
Recipient recipient = message.getRecipients().getPrimaryRecipient(); Recipient recipient = message.getRecipients().getPrimaryRecipient();
RecipientDevice recipientDevice = new RecipientDevice(recipient.getRecipientId(), RecipientDevice.DEFAULT_DEVICE_ID);
String body = message.getMessageBody(); String body = message.getMessageBody();
SmsTransportDetails transportDetails = new SmsTransportDetails(); SmsTransportDetails transportDetails = new SmsTransportDetails();
SessionCipher sessionCipher = SessionCipher.createFor(context, masterSecret, recipient); SessionCipher sessionCipher = SessionCipher.createFor(context, masterSecret, recipientDevice);
byte[] paddedPlaintext = transportDetails.getPaddedMessageBody(body.getBytes()); byte[] paddedPlaintext = transportDetails.getPaddedMessageBody(body.getBytes());
CiphertextMessage ciphertextMessage = sessionCipher.encrypt(paddedPlaintext); CiphertextMessage ciphertextMessage = sessionCipher.encrypt(paddedPlaintext);
String encodedCiphertext = new String(transportDetails.getEncodedMessage(ciphertextMessage.serialize())); String encodedCiphertext = new String(transportDetails.getEncodedMessage(ciphertextMessage.serialize()));

View File

@ -23,21 +23,18 @@ import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.mms.MmsSendResult; import org.thoughtcrime.securesms.mms.MmsSendResult;
import org.thoughtcrime.securesms.push.PushServiceSocketFactory; import org.thoughtcrime.securesms.push.PushServiceSocketFactory;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.textsecure.crypto.MasterSecret; import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.directory.Directory; import org.whispersystems.textsecure.directory.Directory;
import org.whispersystems.textsecure.directory.NotInDirectoryException; import org.whispersystems.textsecure.directory.NotInDirectoryException;
import org.whispersystems.textsecure.push.ContactTokenDetails; import org.whispersystems.textsecure.push.ContactTokenDetails;
import org.whispersystems.textsecure.push.PushDestination;
import org.whispersystems.textsecure.push.PushServiceSocket; import org.whispersystems.textsecure.push.PushServiceSocket;
import org.whispersystems.textsecure.util.InvalidNumberException; import org.whispersystems.textsecure.util.InvalidNumberException;
import java.io.IOException; 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; import ws.com.google.android.mms.pdu.SendReq;
public class UniversalTransport { public class UniversalTransport {
@ -85,17 +82,22 @@ public class UniversalTransport {
public MmsSendResult deliver(SendReq mediaMessage, long threadId) public MmsSendResult deliver(SendReq mediaMessage, long threadId)
throws UndeliverableMessageException throws UndeliverableMessageException
{ {
if (Util.isEmpty(mediaMessage.getTo())) {
throw new UndeliverableMessageException("No destination specified");
}
if (!TextSecurePreferences.isPushRegistered(context)) { if (!TextSecurePreferences.isPushRegistered(context)) {
return mmsTransport.deliver(mediaMessage); return mmsTransport.deliver(mediaMessage);
} }
try { if (isMultipleRecipients(mediaMessage)) {
List<PushDestination> destinations = getMediaDestinations(mediaMessage); return mmsTransport.deliver(mediaMessage);
}
if (isPushTransport(destinations)) { if (isPushTransport(mediaMessage.getTo()[0].getString())) {
try { try {
Log.w("UniversalTransport", "Delivering media message with GCM..."); Log.w("UniversalTransport", "Delivering media message with GCM...");
pushTransport.deliver(mediaMessage, destinations, threadId); pushTransport.deliver(mediaMessage, threadId);
return new MmsSendResult("push".getBytes("UTF-8"), 0, true); return new MmsSendResult("push".getBytes("UTF-8"), 0, true);
} catch (IOException ioe) { } catch (IOException ioe) {
Log.w("UniversalTransport", ioe); Log.w("UniversalTransport", ioe);
@ -105,40 +107,31 @@ public class UniversalTransport {
Log.w("UniversalTransport", "Delivering media message with MMS..."); Log.w("UniversalTransport", "Delivering media message with MMS...");
return mmsTransport.deliver(mediaMessage); return mmsTransport.deliver(mediaMessage);
} }
} catch (InvalidNumberException e) {
Log.w("UniversalTransport", e);
return mmsTransport.deliver(mediaMessage);
}
} }
private List<PushDestination> getMediaDestinations(SendReq mediaMessage) public boolean isMultipleRecipients(SendReq mediaMessage) {
throws InvalidNumberException int recipientCount = 0;
{
String localNumber = TextSecurePreferences.getLocalNumber(context);
LinkedList<PushDestination> destinations = new LinkedList<PushDestination>();
if (mediaMessage.getTo() != null) { if (mediaMessage.getTo() != null) {
for (EncodedStringValue to : mediaMessage.getTo()) { recipientCount += mediaMessage.getTo().length;
destinations.add(PushDestination.create(context, localNumber, to.getString()));
}
} }
if (mediaMessage.getCc() != null) { if (mediaMessage.getCc() != null) {
for (EncodedStringValue cc : mediaMessage.getCc()) { recipientCount += mediaMessage.getCc().length;
destinations.add(PushDestination.create(context, localNumber, cc.getString()));
}
} }
if (mediaMessage.getBcc() != null) { if (mediaMessage.getBcc() != null) {
for (EncodedStringValue bcc : mediaMessage.getBcc()) { recipientCount += mediaMessage.getBcc().length;
destinations.add(PushDestination.create(context, localNumber, bcc.getString()));
}
} }
return destinations; return recipientCount > 1;
} }
private boolean isPushTransport(String destination) { private boolean isPushTransport(String destination) {
if (GroupUtil.isEncodedGroup(destination)) {
return true;
}
Directory directory = Directory.getInstance(context); Directory directory = Directory.getInstance(context);
try { try {
@ -163,14 +156,4 @@ public class UniversalTransport {
} }
} }
} }
private boolean isPushTransport(List<PushDestination> destinations) {
for (PushDestination destination : destinations) {
if (!isPushTransport(destination.getNumber())) {
return false;
}
}
return true;
}
} }

View File

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

View File

@ -34,6 +34,12 @@ public class NumberUtil {
return PhoneNumberUtils.isWellFormedSmsAddress(number) || isValidEmail(number); 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) { public static String filterNumber(String number) {
if (number == null) return null; if (number == null) return null;