Support for an 'end session' protocol message.

1) On the push side, this message is a flag in PushMessageContent.
   Any secure message with that flag will terminate the current
   sessin.

2) On the SMS side, there is an "end session" wire type and
   the convention that a message with this wire type must be
   secure and contain the string "TERMINATE."
This commit is contained in:
Moxie Marlinspike 2014-02-19 13:46:49 -08:00
parent 0688dd0c2c
commit 19dddd7adf
32 changed files with 400 additions and 84 deletions

View file

@ -10,7 +10,6 @@ message IncomingPushMessageSignal {
KEY_EXCHANGE = 2; KEY_EXCHANGE = 2;
PREKEY_BUNDLE = 3; PREKEY_BUNDLE = 3;
PLAINTEXT = 4; PLAINTEXT = 4;
ADVISORY = 5;
} }
optional Type type = 1; optional Type type = 1;
optional string source = 2; optional string source = 2;
@ -23,9 +22,9 @@ message IncomingPushMessageSignal {
message PushMessageContent { message PushMessageContent {
message AttachmentPointer { message AttachmentPointer {
optional fixed64 id = 1; optional fixed64 id = 1;
optional string contentType = 2; optional string contentType = 2;
optional bytes key = 3; optional bytes key = 3;
} }
message GroupContext { message GroupContext {
@ -37,14 +36,19 @@ message PushMessageContent {
ADD = 4; ADD = 4;
QUIT = 5; QUIT = 5;
} }
optional bytes id = 1; optional bytes id = 1;
optional Type type = 2; optional Type type = 2;
optional string name = 3; optional string name = 3;
repeated string members = 4; repeated string members = 4;
optional AttachmentPointer avatar = 5; optional AttachmentPointer avatar = 5;
} }
optional string body = 1; enum Flags {
END_SESSION = 1;
}
optional string body = 1;
repeated AttachmentPointer attachments = 2; repeated AttachmentPointer attachments = 2;
optional GroupContext group = 3; optional GroupContext group = 3;
optional uint32 flags = 4;
} }

View file

@ -23,4 +23,4 @@ message KeyExchangeMessage {
optional bytes baseKey = 2; optional bytes baseKey = 2;
optional bytes ephemeralKey = 3; optional bytes ephemeralKey = 3;
optional bytes identityKey = 4; optional bytes identityKey = 4;
} }

View file

@ -31,6 +31,7 @@ import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidAlgorithmParameterException; import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException; import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.text.ParseException;
import java.util.Arrays; import java.util.Arrays;
/** /**
@ -118,6 +119,8 @@ public class AttachmentCipher {
throw new AssertionError(e); throw new AssertionError(e);
} catch (BadPaddingException e) { } catch (BadPaddingException e) {
throw new InvalidMessageException(e); throw new InvalidMessageException(e);
} catch (ParseException e) {
throw new InvalidMessageException(e);
} }
} }

View file

@ -15,6 +15,7 @@ import org.whispersystems.textsecure.util.Hex;
import org.whispersystems.textsecure.util.Util; import org.whispersystems.textsecure.util.Util;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.text.ParseException;
import java.util.Arrays; import java.util.Arrays;
import javax.crypto.Mac; import javax.crypto.Mac;
@ -38,7 +39,7 @@ public class WhisperMessageV2 implements CiphertextMessage {
byte[] mac = messageParts[2]; byte[] mac = messageParts[2];
if (Conversions.highBitsToInt(version) != CURRENT_VERSION) { if (Conversions.highBitsToInt(version) != CURRENT_VERSION) {
throw new InvalidMessageException("Unknown version: " + Conversions.lowBitsToInt(version)); throw new InvalidMessageException("Unknown version: " + Conversions.highBitsToInt(version));
} }
WhisperMessage whisperMessage = WhisperMessage.parseFrom(message); WhisperMessage whisperMessage = WhisperMessage.parseFrom(message);
@ -59,6 +60,8 @@ public class WhisperMessageV2 implements CiphertextMessage {
throw new InvalidMessageException(e); throw new InvalidMessageException(e);
} catch (InvalidKeyException e) { } catch (InvalidKeyException e) {
throw new InvalidMessageException(e); throw new InvalidMessageException(e);
} catch (ParseException e) {
throw new InvalidMessageException(e);
} }
} }

View file

@ -70,7 +70,6 @@ public final class PushMessageProtos {
KEY_EXCHANGE(2, 2), KEY_EXCHANGE(2, 2),
PREKEY_BUNDLE(3, 3), PREKEY_BUNDLE(3, 3),
PLAINTEXT(4, 4), PLAINTEXT(4, 4),
ADVISORY(5, 5),
; ;
public static final int UNKNOWN_VALUE = 0; public static final int UNKNOWN_VALUE = 0;
@ -78,7 +77,6 @@ public final class PushMessageProtos {
public static final int KEY_EXCHANGE_VALUE = 2; public static final int KEY_EXCHANGE_VALUE = 2;
public static final int PREKEY_BUNDLE_VALUE = 3; public static final int PREKEY_BUNDLE_VALUE = 3;
public static final int PLAINTEXT_VALUE = 4; public static final int PLAINTEXT_VALUE = 4;
public static final int ADVISORY_VALUE = 5;
public final int getNumber() { return value; } public final int getNumber() { return value; }
@ -90,7 +88,6 @@ public final class PushMessageProtos {
case 2: return KEY_EXCHANGE; case 2: return KEY_EXCHANGE;
case 3: return PREKEY_BUNDLE; case 3: return PREKEY_BUNDLE;
case 4: return PLAINTEXT; case 4: return PLAINTEXT;
case 5: return ADVISORY;
default: return null; default: return null;
} }
} }
@ -121,7 +118,7 @@ public final class PushMessageProtos {
} }
private static final Type[] VALUES = { private static final Type[] VALUES = {
UNKNOWN, CIPHERTEXT, KEY_EXCHANGE, PREKEY_BUNDLE, PLAINTEXT, ADVISORY, UNKNOWN, CIPHERTEXT, KEY_EXCHANGE, PREKEY_BUNDLE, PLAINTEXT,
}; };
public static Type valueOf( public static Type valueOf(
@ -819,6 +816,10 @@ public final class PushMessageProtos {
boolean hasGroup(); boolean hasGroup();
org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent.GroupContext getGroup(); org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent.GroupContext getGroup();
org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent.GroupContextOrBuilder getGroupOrBuilder(); org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent.GroupContextOrBuilder getGroupOrBuilder();
// optional uint32 flags = 4;
boolean hasFlags();
int getFlags();
} }
public static final class PushMessageContent extends public static final class PushMessageContent extends
com.google.protobuf.GeneratedMessage com.google.protobuf.GeneratedMessage
@ -848,6 +849,72 @@ public final class PushMessageProtos {
return org.whispersystems.textsecure.push.PushMessageProtos.internal_static_textsecure_PushMessageContent_fieldAccessorTable; return org.whispersystems.textsecure.push.PushMessageProtos.internal_static_textsecure_PushMessageContent_fieldAccessorTable;
} }
public enum Flags
implements com.google.protobuf.ProtocolMessageEnum {
END_SESSION(0, 1),
;
public static final int END_SESSION_VALUE = 1;
public final int getNumber() { return value; }
public static Flags valueOf(int value) {
switch (value) {
case 1: return END_SESSION;
default: return null;
}
}
public static com.google.protobuf.Internal.EnumLiteMap<Flags>
internalGetValueMap() {
return internalValueMap;
}
private static com.google.protobuf.Internal.EnumLiteMap<Flags>
internalValueMap =
new com.google.protobuf.Internal.EnumLiteMap<Flags>() {
public Flags findValueByNumber(int number) {
return Flags.valueOf(number);
}
};
public final com.google.protobuf.Descriptors.EnumValueDescriptor
getValueDescriptor() {
return getDescriptor().getValues().get(index);
}
public final com.google.protobuf.Descriptors.EnumDescriptor
getDescriptorForType() {
return getDescriptor();
}
public static final com.google.protobuf.Descriptors.EnumDescriptor
getDescriptor() {
return org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent.getDescriptor().getEnumTypes().get(0);
}
private static final Flags[] VALUES = {
END_SESSION,
};
public static Flags valueOf(
com.google.protobuf.Descriptors.EnumValueDescriptor desc) {
if (desc.getType() != getDescriptor()) {
throw new java.lang.IllegalArgumentException(
"EnumValueDescriptor is not for this type.");
}
return VALUES[desc.getIndex()];
}
private final int index;
private final int value;
private Flags(int index, int value) {
this.index = index;
this.value = value;
}
// @@protoc_insertion_point(enum_scope:textsecure.PushMessageContent.Flags)
}
public interface AttachmentPointerOrBuilder public interface AttachmentPointerOrBuilder
extends com.google.protobuf.MessageOrBuilder { extends com.google.protobuf.MessageOrBuilder {
@ -2243,10 +2310,21 @@ public final class PushMessageProtos {
return group_; return group_;
} }
// optional uint32 flags = 4;
public static final int FLAGS_FIELD_NUMBER = 4;
private int flags_;
public boolean hasFlags() {
return ((bitField0_ & 0x00000004) == 0x00000004);
}
public int getFlags() {
return flags_;
}
private void initFields() { private void initFields() {
body_ = ""; body_ = "";
attachments_ = java.util.Collections.emptyList(); attachments_ = java.util.Collections.emptyList();
group_ = org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent.GroupContext.getDefaultInstance(); group_ = org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent.GroupContext.getDefaultInstance();
flags_ = 0;
} }
private byte memoizedIsInitialized = -1; private byte memoizedIsInitialized = -1;
public final boolean isInitialized() { public final boolean isInitialized() {
@ -2269,6 +2347,9 @@ public final class PushMessageProtos {
if (((bitField0_ & 0x00000002) == 0x00000002)) { if (((bitField0_ & 0x00000002) == 0x00000002)) {
output.writeMessage(3, group_); output.writeMessage(3, group_);
} }
if (((bitField0_ & 0x00000004) == 0x00000004)) {
output.writeUInt32(4, flags_);
}
getUnknownFields().writeTo(output); getUnknownFields().writeTo(output);
} }
@ -2290,6 +2371,10 @@ public final class PushMessageProtos {
size += com.google.protobuf.CodedOutputStream size += com.google.protobuf.CodedOutputStream
.computeMessageSize(3, group_); .computeMessageSize(3, group_);
} }
if (((bitField0_ & 0x00000004) == 0x00000004)) {
size += com.google.protobuf.CodedOutputStream
.computeUInt32Size(4, flags_);
}
size += getUnknownFields().getSerializedSize(); size += getUnknownFields().getSerializedSize();
memoizedSerializedSize = size; memoizedSerializedSize = size;
return size; return size;
@ -2430,6 +2515,8 @@ public final class PushMessageProtos {
groupBuilder_.clear(); groupBuilder_.clear();
} }
bitField0_ = (bitField0_ & ~0x00000004); bitField0_ = (bitField0_ & ~0x00000004);
flags_ = 0;
bitField0_ = (bitField0_ & ~0x00000008);
return this; return this;
} }
@ -2489,6 +2576,10 @@ public final class PushMessageProtos {
} else { } else {
result.group_ = groupBuilder_.build(); result.group_ = groupBuilder_.build();
} }
if (((from_bitField0_ & 0x00000008) == 0x00000008)) {
to_bitField0_ |= 0x00000004;
}
result.flags_ = flags_;
result.bitField0_ = to_bitField0_; result.bitField0_ = to_bitField0_;
onBuilt(); onBuilt();
return result; return result;
@ -2537,6 +2628,9 @@ public final class PushMessageProtos {
if (other.hasGroup()) { if (other.hasGroup()) {
mergeGroup(other.getGroup()); mergeGroup(other.getGroup());
} }
if (other.hasFlags()) {
setFlags(other.getFlags());
}
this.mergeUnknownFields(other.getUnknownFields()); this.mergeUnknownFields(other.getUnknownFields());
return this; return this;
} }
@ -2588,6 +2682,11 @@ public final class PushMessageProtos {
setGroup(subBuilder.buildPartial()); setGroup(subBuilder.buildPartial());
break; break;
} }
case 32: {
bitField0_ |= 0x00000008;
flags_ = input.readUInt32();
break;
}
} }
} }
} }
@ -2906,6 +3005,27 @@ public final class PushMessageProtos {
return groupBuilder_; return groupBuilder_;
} }
// optional uint32 flags = 4;
private int flags_ ;
public boolean hasFlags() {
return ((bitField0_ & 0x00000008) == 0x00000008);
}
public int getFlags() {
return flags_;
}
public Builder setFlags(int value) {
bitField0_ |= 0x00000008;
flags_ = value;
onChanged();
return this;
}
public Builder clearFlags() {
bitField0_ = (bitField0_ & ~0x00000008);
flags_ = 0;
onChanged();
return this;
}
// @@protoc_insertion_point(builder_scope:textsecure.PushMessageContent) // @@protoc_insertion_point(builder_scope:textsecure.PushMessageContent)
} }
@ -2947,28 +3067,28 @@ 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\"\225\002\n\031IncomingPushMessageSignal\0228\n\004t" + "ecure\"\207\002\n\031IncomingPushMessageSignal\0228\n\004t" +
"ype\030\001 \001(\0162*.textsecure.IncomingPushMessa" + "ype\030\001 \001(\0162*.textsecure.IncomingPushMessa" +
"geSignal.Type\022\016\n\006source\030\002 \001(\t\022\024\n\014sourceD" + "geSignal.Type\022\016\n\006source\030\002 \001(\t\022\024\n\014sourceD" +
"evice\030\007 \001(\r\022\r\n\005relay\030\003 \001(\t\022\021\n\ttimestamp\030" + "evice\030\007 \001(\r\022\r\n\005relay\030\003 \001(\t\022\021\n\ttimestamp\030" +
"\005 \001(\004\022\017\n\007message\030\006 \001(\014\"e\n\004Type\022\013\n\007UNKNOW" + "\005 \001(\004\022\017\n\007message\030\006 \001(\014\"W\n\004Type\022\013\n\007UNKNOW" +
"N\020\000\022\016\n\nCIPHERTEXT\020\001\022\020\n\014KEY_EXCHANGE\020\002\022\021\n" + "N\020\000\022\016\n\nCIPHERTEXT\020\001\022\020\n\014KEY_EXCHANGE\020\002\022\021\n" +
"\rPREKEY_BUNDLE\020\003\022\r\n\tPLAINTEXT\020\004\022\014\n\010ADVIS" + "\rPREKEY_BUNDLE\020\003\022\r\n\tPLAINTEXT\020\004\"\234\004\n\022Push" +
"ORY\020\005\"\363\003\n\022PushMessageContent\022\014\n\004body\030\001 \001" + "MessageContent\022\014\n\004body\030\001 \001(\t\022E\n\013attachme" +
"(\t\022E\n\013attachments\030\002 \003(\01320.textsecure.Pus", "nts\030\002 \003(\01320.textsecure.PushMessageConten",
"hMessageContent.AttachmentPointer\022:\n\005gro" + "t.AttachmentPointer\022:\n\005group\030\003 \001(\0132+.tex" +
"up\030\003 \001(\0132+.textsecure.PushMessageContent" + "tsecure.PushMessageContent.GroupContext\022" +
".GroupContext\032A\n\021AttachmentPointer\022\n\n\002id" + "\r\n\005flags\030\004 \001(\r\032A\n\021AttachmentPointer\022\n\n\002i" +
"\030\001 \001(\006\022\023\n\013contentType\030\002 \001(\t\022\013\n\003key\030\003 \001(\014" + "d\030\001 \001(\006\022\023\n\013contentType\030\002 \001(\t\022\013\n\003key\030\003 \001(" +
"\032\210\002\n\014GroupContext\022\n\n\002id\030\001 \001(\014\022>\n\004type\030\002 " + "\014\032\210\002\n\014GroupContext\022\n\n\002id\030\001 \001(\014\022>\n\004type\030\002" +
"\001(\01620.textsecure.PushMessageContent.Grou" + " \001(\01620.textsecure.PushMessageContent.Gro" +
"pContext.Type\022\014\n\004name\030\003 \001(\t\022\017\n\007members\030\004" + "upContext.Type\022\014\n\004name\030\003 \001(\t\022\017\n\007members\030" +
" \003(\t\022@\n\006avatar\030\005 \001(\01320.textsecure.PushMe" + "\004 \003(\t\022@\n\006avatar\030\005 \001(\01320.textsecure.PushM" +
"ssageContent.AttachmentPointer\"K\n\004Type\022\013" + "essageContent.AttachmentPointer\"K\n\004Type\022" +
"\n\007UNKNOWN\020\000\022\n\n\006CREATE\020\001\022\n\n\006MODIFY\020\002\022\013\n\007D", "\013\n\007UNKNOWN\020\000\022\n\n\006CREATE\020\001\022\n\n\006MODIFY\020\002\022\013\n\007",
"ELIVER\020\003\022\007\n\003ADD\020\004\022\010\n\004QUIT\020\005B7\n\"org.whisp" + "DELIVER\020\003\022\007\n\003ADD\020\004\022\010\n\004QUIT\020\005\"\030\n\005Flags\022\017\n" +
"ersystems.textsecure.pushB\021PushMessagePr" + "\013END_SESSION\020\001B7\n\"org.whispersystems.tex" +
"otos" "tsecure.pushB\021PushMessageProtos"
}; };
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() {
@ -2988,7 +3108,7 @@ public final class PushMessageProtos {
internal_static_textsecure_PushMessageContent_fieldAccessorTable = new internal_static_textsecure_PushMessageContent_fieldAccessorTable = new
com.google.protobuf.GeneratedMessage.FieldAccessorTable( com.google.protobuf.GeneratedMessage.FieldAccessorTable(
internal_static_textsecure_PushMessageContent_descriptor, internal_static_textsecure_PushMessageContent_descriptor,
new java.lang.String[] { "Body", "Attachments", "Group", }, new java.lang.String[] { "Body", "Attachments", "Group", "Flags", },
org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent.class, org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent.class,
org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent.Builder.class); org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent.Builder.class);
internal_static_textsecure_PushMessageContent_AttachmentPointer_descriptor = internal_static_textsecure_PushMessageContent_AttachmentPointer_descriptor =

View file

@ -13,6 +13,7 @@ import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.text.ParseException;
import java.util.Collection; import java.util.Collection;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
@ -45,7 +46,15 @@ public class Util {
return parts; return parts;
} }
public static byte[][] split(byte[] input, int firstLength, int secondLength, int thirdLength) { public static byte[][] split(byte[] input, int firstLength, int secondLength, int thirdLength)
throws ParseException
{
if (input == null || firstLength < 0 || secondLength < 0 || thirdLength < 0 ||
input.length < firstLength + secondLength + thirdLength)
{
throw new ParseException("Input too small: " + (input == null ? null : Hex.toString(input)), 0);
}
byte[][] parts = new byte[3][]; byte[][] parts = new byte[3][];
parts[0] = new byte[firstLength]; parts[0] = new byte[firstLength];

View file

@ -80,6 +80,7 @@ import org.thoughtcrime.securesms.recipients.Recipients;
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.OutgoingEncryptedMessage; import org.thoughtcrime.securesms.sms.OutgoingEncryptedMessage;
import org.thoughtcrime.securesms.sms.OutgoingEndSessionMessage;
import org.thoughtcrime.securesms.sms.OutgoingTextMessage; import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
import org.thoughtcrime.securesms.util.ActionBarUtil; import org.thoughtcrime.securesms.util.ActionBarUtil;
import org.thoughtcrime.securesms.util.BitmapDecodingException; import org.thoughtcrime.securesms.util.BitmapDecodingException;
@ -93,7 +94,9 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.textsecure.crypto.InvalidMessageException; import org.whispersystems.textsecure.crypto.InvalidMessageException;
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.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.Util; import org.whispersystems.textsecure.util.Util;
import java.io.IOException; import java.io.IOException;
@ -368,9 +371,23 @@ public class ConversationActivity extends PassphraseRequiredSherlockFragmentActi
@Override @Override
public void onClick(DialogInterface dialog, int which) { public void onClick(DialogInterface dialog, int which) {
if (isSingleConversation()) { if (isSingleConversation()) {
Session.abortSessionFor(ConversationActivity.this, getRecipients().getPrimaryRecipient()); ConversationActivity self = ConversationActivity.this;
initializeSecurity(); Recipient recipient = getRecipients().getPrimaryRecipient();
initializeTitleBar();
if (SessionRecordV2.hasSession(self, masterSecret,
recipient.getRecipientId(),
RecipientDevice.DEFAULT_DEVICE_ID))
{
OutgoingEndSessionMessage endSessionMessage =
new OutgoingEndSessionMessage(new OutgoingTextMessage(getRecipients(), "TERMINATE"));
long allocatedThreadId = MessageSender.send(self, masterSecret,
endSessionMessage, threadId);
sendComplete(recipients, allocatedThreadId, allocatedThreadId != self.threadId);
} else {
Session.abortSessionFor(self, recipient);
}
} }
} }
}); });

View file

@ -236,7 +236,7 @@ public class ReceiveKeyActivity extends Activity {
DecryptingQueue.scheduleDecryption(ReceiveKeyActivity.this, masterSecret, messageId, DecryptingQueue.scheduleDecryption(ReceiveKeyActivity.this, masterSecret, messageId,
threadId, recipient.getNumber(), recipientDeviceId, threadId, recipient.getNumber(), recipientDeviceId,
messageBody, true, false); messageBody, true, false, 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

@ -81,11 +81,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, int deviceId, long messageId, long threadId, String originator, int deviceId,
String body, boolean isSecureMessage, boolean isKeyExchange) String body, boolean isSecureMessage, boolean isKeyExchange,
boolean isEndSession)
{ {
DecryptionWorkItem runnable = new DecryptionWorkItem(context, masterSecret, messageId, threadId, DecryptionWorkItem runnable = new DecryptionWorkItem(context, masterSecret, messageId, threadId,
originator, deviceId, body, originator, deviceId, body,
isSecureMessage, isKeyExchange); isSecureMessage, isKeyExchange, isEndSession);
executor.execute(runnable); executor.execute(runnable);
} }
@ -167,10 +168,11 @@ public class DecryptingQueue {
int originatorDeviceId = record.getRecipientDeviceId(); int originatorDeviceId = record.getRecipientDeviceId();
boolean isSecureMessage = record.isSecure(); boolean isSecureMessage = record.isSecure();
boolean isKeyExchange = record.isKeyExchange(); boolean isKeyExchange = record.isKeyExchange();
boolean isEndSession = record.isEndSession();
scheduleDecryption(context, masterSecret, messageId, threadId, scheduleDecryption(context, masterSecret, messageId, threadId,
originator, originatorDeviceId, body, originator, originatorDeviceId, body,
isSecureMessage, isKeyExchange); isSecureMessage, isKeyExchange, isEndSession);
} }
private static class PushDecryptionWorkItem implements Runnable { private static class PushDecryptionWorkItem implements Runnable {
@ -332,10 +334,11 @@ public class DecryptingQueue {
private final int deviceId; private final int deviceId;
private final boolean isSecureMessage; private final boolean isSecureMessage;
private final boolean isKeyExchange; private final boolean isKeyExchange;
private final boolean isEndSession;
public DecryptionWorkItem(Context context, MasterSecret masterSecret, long messageId, long threadId, public DecryptionWorkItem(Context context, MasterSecret masterSecret, long messageId, long threadId,
String originator, int deviceId, String body, boolean isSecureMessage, String originator, int deviceId, String body, boolean isSecureMessage,
boolean isKeyExchange) boolean isKeyExchange, boolean isEndSession)
{ {
this.context = context; this.context = context;
this.messageId = messageId; this.messageId = messageId;
@ -346,6 +349,7 @@ public class DecryptingQueue {
this.deviceId = deviceId; this.deviceId = deviceId;
this.isSecureMessage = isSecureMessage; this.isSecureMessage = isSecureMessage;
this.isKeyExchange = isKeyExchange; this.isKeyExchange = isKeyExchange;
this.isEndSession = isEndSession;
} }
private void handleRemoteAsymmetricEncrypt() { private void handleRemoteAsymmetricEncrypt() {
@ -368,6 +372,13 @@ public class DecryptingQueue {
byte[] paddedPlaintext = sessionCipher.decrypt(decodedCiphertext); byte[] paddedPlaintext = sessionCipher.decrypt(decodedCiphertext);
plaintextBody = new String(transportDetails.getStrippedPaddingMessageBody(paddedPlaintext)); plaintextBody = new String(transportDetails.getStrippedPaddingMessageBody(paddedPlaintext));
if (isEndSession &&
"TERMINATE".equals(plaintextBody) &&
SessionRecordV2.hasSession(context, masterSecret, recipientDevice))
{
Session.abortSessionFor(context, recipient);
}
} catch (InvalidMessageException e) { } catch (InvalidMessageException e) {
Log.w("DecryptionQueue", e); Log.w("DecryptionQueue", e);
database.markAsDecryptFailed(messageId); database.markAsDecryptFailed(messageId);
@ -441,7 +452,7 @@ public class DecryptingQueue {
@Override @Override
public void run() { public void run() {
if (isSecureMessage) { if (isSecureMessage || isEndSession) {
handleRemoteAsymmetricEncrypt(); handleRemoteAsymmetricEncrypt();
} else { } else {
handleLocalAsymmetricEncrypt(); handleLocalAsymmetricEncrypt();

View file

@ -18,10 +18,12 @@
package org.thoughtcrime.securesms.crypto; package org.thoughtcrime.securesms.crypto;
import android.content.Context; import android.content.Context;
import android.content.Intent;
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.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.service.KeyCachingService;
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; import org.whispersystems.textsecure.storage.RecipientDevice;
@ -45,4 +47,12 @@ public abstract class KeyExchangeProcessor {
return new KeyExchangeProcessorV2(context, masterSecret, recipientDevice); return new KeyExchangeProcessorV2(context, masterSecret, recipientDevice);
} }
} }
public static void broadcastSecurityUpdateEvent(Context context, long threadId) {
Intent intent = new Intent(SECURITY_UPDATE_EVENT);
intent.putExtra("thread_id", threadId);
intent.setPackage(context.getPackageName());
context.sendBroadcast(intent, KeyCachingService.KEY_PERMISSION);
}
} }

View file

@ -1,7 +1,6 @@
package org.thoughtcrime.securesms.crypto; package org.thoughtcrime.securesms.crypto;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.util.Log; import android.util.Log;
import org.thoughtcrime.securesms.crypto.protocol.KeyExchangeMessage; import org.thoughtcrime.securesms.crypto.protocol.KeyExchangeMessage;
@ -9,7 +8,6 @@ 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.recipients.RecipientFactory;
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;
import org.whispersystems.textsecure.crypto.IdentityKey; import org.whispersystems.textsecure.crypto.IdentityKey;
@ -129,13 +127,6 @@ public class KeyExchangeProcessorV1 extends KeyExchangeProcessor {
broadcastSecurityUpdateEvent(context, threadId); broadcastSecurityUpdateEvent(context, threadId);
} }
private static void broadcastSecurityUpdateEvent(Context context, long threadId) {
Intent intent = new Intent(SECURITY_UPDATE_EVENT);
intent.putExtra("thread_id", threadId);
intent.setPackage(context.getPackageName());
context.sendBroadcast(intent, KeyCachingService.KEY_PERMISSION);
}
public LocalKeyRecord initializeRecordFor(Context context, public LocalKeyRecord initializeRecordFor(Context context,
MasterSecret masterSecret, MasterSecret masterSecret,
CanonicalRecipient recipient) CanonicalRecipient recipient)

View file

@ -1,7 +1,6 @@
package org.thoughtcrime.securesms.crypto; package org.thoughtcrime.securesms.crypto;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.util.Log; import android.util.Log;
import org.thoughtcrime.securesms.crypto.protocol.KeyExchangeMessage; import org.thoughtcrime.securesms.crypto.protocol.KeyExchangeMessage;
@ -9,7 +8,6 @@ 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.recipients.RecipientFactory;
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;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
@ -230,11 +228,4 @@ public class KeyExchangeProcessorV2 extends KeyExchangeProcessor {
} }
} }
private static void broadcastSecurityUpdateEvent(Context context, long threadId) {
Intent intent = new Intent(KeyExchangeProcessorV1.SECURITY_UPDATE_EVENT);
intent.putExtra("thread_id", threadId);
intent.setPackage(context.getPackageName());
context.sendBroadcast(intent, KeyCachingService.KEY_PERMISSION);
}
} }

View file

@ -73,7 +73,7 @@ public class EncryptingSmsDatabase extends SmsDatabase {
{ {
long type = Types.BASE_INBOX_TYPE; long type = Types.BASE_INBOX_TYPE;
if (!message.isSecureMessage()) { if (!message.isSecureMessage() && !message.isEndSession()) {
type |= Types.ENCRYPTION_SYMMETRIC_BIT; type |= Types.ENCRYPTION_SYMMETRIC_BIT;
message = message.withMessageBody(getEncryptedBody(masterSecret, message.getMessageBody())); message = message.withMessageBody(getEncryptedBody(masterSecret, message.getMessageBody()));
} }

View file

@ -39,6 +39,7 @@ public interface MmsSmsColumns {
// Secure Message Information // Secure Message Information
protected static final long SECURE_MESSAGE_BIT = 0x800000; protected static final long SECURE_MESSAGE_BIT = 0x800000;
protected static final long END_SESSION_BIT = 0x400000;
// Encrypted Storage Information // Encrypted Storage Information
protected static final long ENCRYPTION_MASK = 0xFF000000; protected static final long ENCRYPTION_MASK = 0xFF000000;
@ -75,6 +76,10 @@ public interface MmsSmsColumns {
return (type & SECURE_MESSAGE_BIT) != 0; return (type & SECURE_MESSAGE_BIT) != 0;
} }
public static boolean isEndSessionType(long type) {
return (type & END_SESSION_BIT) != 0;
}
public static boolean isKeyExchangeType(long type) { public static boolean isKeyExchangeType(long type) {
return (type & KEY_EXCHANGE_BIT) != 0; return (type & KEY_EXCHANGE_BIT) != 0;
} }

View file

@ -256,6 +256,10 @@ public class SmsDatabase extends Database implements MmsSmsColumns {
} else if (message.isSecureMessage()) { } else if (message.isSecureMessage()) {
type |= Types.SECURE_MESSAGE_BIT; type |= Types.SECURE_MESSAGE_BIT;
type |= Types.ENCRYPTION_REMOTE_BIT; type |= Types.ENCRYPTION_REMOTE_BIT;
} else if (message.isEndSession()) {
type |= Types.END_SESSION_BIT;
type |= Types.SECURE_MESSAGE_BIT;
type |= Types.ENCRYPTION_REMOTE_BIT;
} }
Recipients recipients; Recipients recipients;
@ -328,6 +332,7 @@ public class SmsDatabase extends Database implements MmsSmsColumns {
protected List<Long> insertMessageOutbox(long threadId, OutgoingTextMessage message, long type) { protected List<Long> insertMessageOutbox(long threadId, OutgoingTextMessage message, long type) {
if (message.isKeyExchange()) type |= Types.KEY_EXCHANGE_BIT; if (message.isKeyExchange()) type |= Types.KEY_EXCHANGE_BIT;
else if (message.isSecureMessage()) type |= Types.SECURE_MESSAGE_BIT; else if (message.isSecureMessage()) type |= Types.SECURE_MESSAGE_BIT;
else if (message.isEndSession()) type |= Types.END_SESSION_BIT;
long date = System.currentTimeMillis(); long date = System.currentTimeMillis();
List<Long> messageIds = new LinkedList<Long>(); List<Long> messageIds = new LinkedList<Long>();

View file

@ -84,6 +84,10 @@ public abstract class DisplayRecord {
return SmsDatabase.Types.isKeyExchangeType(type); return SmsDatabase.Types.isKeyExchangeType(type);
} }
public boolean isEndSession() {
return SmsDatabase.Types.isEndSessionType(type);
}
public int getGroupAction() { public int getGroupAction() {
return groupAction; return groupAction;
} }

View file

@ -79,6 +79,9 @@ public class SmsMessageRecord extends MessageRecord {
return emphasisAdded(context.getString(R.string.MessageDisplayHelper_message_encrypted_for_non_existing_session)); return emphasisAdded(context.getString(R.string.MessageDisplayHelper_message_encrypted_for_non_existing_session));
} else if (!getBody().isPlaintext()) { } else if (!getBody().isPlaintext()) {
return emphasisAdded(context.getString(R.string.MessageNotifier_encrypted_message)); return emphasisAdded(context.getString(R.string.MessageNotifier_encrypted_message));
} else if (SmsDatabase.Types.isEndSessionType(type)) {
// TODO jake is going to fix this up
return new SpannableString("Session closed!");
} else if (isOutgoing() && Tag.isTagged(getBody().getBody())) { } else if (isOutgoing() && Tag.isTagged(getBody().getBody())) {
return new SpannableString(Tag.stripTag(getBody().getBody())); return new SpannableString(Tag.stripTag(getBody().getBody()));
} else { } else {

View file

@ -78,6 +78,9 @@ public class ThreadRecord extends DisplayRecord {
return emphasisAdded(context.getString(R.string.MessageDisplayHelper_message_encrypted_for_non_existing_session)); return emphasisAdded(context.getString(R.string.MessageDisplayHelper_message_encrypted_for_non_existing_session));
} else if (!getBody().isPlaintext()) { } else if (!getBody().isPlaintext()) {
return emphasisAdded(context.getString(R.string.MessageNotifier_encrypted_message)); return emphasisAdded(context.getString(R.string.MessageNotifier_encrypted_message));
} else if (SmsDatabase.Types.isEndSessionType(type)) {
// TODO jake is going to fix this up
return emphasisAdded("Session closed!");
} else { } else {
if (Util.isEmpty(getBody().getBody())) { if (Util.isEmpty(getBody().getBody())) {
return new SpannableString(context.getString(R.string.MessageNotifier_no_subject)); return new SpannableString(context.getString(R.string.MessageNotifier_no_subject));

View file

@ -0,0 +1,8 @@
package org.thoughtcrime.securesms.protocol;
public class EndSessionWirePrefix extends WirePrefix {
@Override
public String calculatePrefix(String message) {
return super.calculateEndSessionPrefix(message);
}
}

View file

@ -51,6 +51,10 @@ public abstract class WirePrefix {
return verifyPrefix("?TSP", message); return verifyPrefix("?TSP", message);
} }
public static boolean isEndSession(String message) {
return verifyPrefix("?TSE", message);
}
public static String calculateKeyExchangePrefix(String message) { public static String calculateKeyExchangePrefix(String message) {
return calculatePrefix(("?TSK" + message).getBytes(), PREFIX_BYTES); return calculatePrefix(("?TSK" + message).getBytes(), PREFIX_BYTES);
} }
@ -63,6 +67,10 @@ public abstract class WirePrefix {
return calculatePrefix(("?TSP" + message).getBytes(), PREFIX_BYTES); return calculatePrefix(("?TSP" + message).getBytes(), PREFIX_BYTES);
} }
public static String calculateEndSessionPrefix(String message) {
return calculatePrefix(("?TSE" + message).getBytes(), PREFIX_BYTES);
}
private static boolean verifyPrefix(String prefixType, String message) { private static boolean verifyPrefix(String prefixType, String message) {
if (message.length() <= PREFIX_SIZE) if (message.length() <= PREFIX_SIZE)
return false; return false;

View file

@ -8,6 +8,7 @@ import android.util.Pair;
import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.InvalidProtocolBufferException;
import org.thoughtcrime.securesms.crypto.DecryptingQueue; import org.thoughtcrime.securesms.crypto.DecryptingQueue;
import org.thoughtcrime.securesms.crypto.KeyExchangeProcessor;
import org.thoughtcrime.securesms.crypto.KeyExchangeProcessorV2; import org.thoughtcrime.securesms.crypto.KeyExchangeProcessorV2;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.EncryptingSmsDatabase; import org.thoughtcrime.securesms.database.EncryptingSmsDatabase;
@ -20,6 +21,7 @@ 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.sms.IncomingEncryptedMessage; import org.thoughtcrime.securesms.sms.IncomingEncryptedMessage;
import org.thoughtcrime.securesms.sms.IncomingEndSessionMessage;
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;
@ -34,6 +36,7 @@ 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.storage.RecipientDevice;
import org.whispersystems.textsecure.storage.Session;
import ws.com.google.android.mms.MmsException; import ws.com.google.android.mms.MmsException;
@ -150,7 +153,10 @@ public class PushReceiver {
try { try {
PushMessageContent messageContent = PushMessageContent.parseFrom(message.getBody()); PushMessageContent messageContent = PushMessageContent.parseFrom(message.getBody());
if (messageContent.hasGroup()) { if (secure && (messageContent.getFlags() & PushMessageContent.Flags.END_SESSION_VALUE) != 0) {
Log.w("PushReceiver", "Received end session message...");
handleEndSessionMessage(masterSecret, message, messageContent);
} else if (messageContent.hasGroup()) {
Log.w("PushReceiver", "Received push group message..."); Log.w("PushReceiver", "Received push group message...");
handleReceivedGroupMessage(masterSecret, message, messageContent, secure); handleReceivedGroupMessage(masterSecret, message, messageContent, secure);
} else if (messageContent.getAttachmentsCount() > 0) { } else if (messageContent.getAttachmentsCount() > 0) {
@ -222,6 +228,27 @@ public class PushReceiver {
} }
} }
private void handleEndSessionMessage(MasterSecret masterSecret,
IncomingPushMessage message,
PushMessageContent messageContent)
{
try {
Recipient recipient = RecipientFactory.getRecipientsFromString(context, message.getSource(), true).getPrimaryRecipient();
IncomingTextMessage incomingTextMessage = new IncomingTextMessage(message, "", null);
IncomingEndSessionMessage incomingEndSessionMessage = new IncomingEndSessionMessage(incomingTextMessage);
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
Pair<Long, Long> messageAndThreadId = database.insertMessageInbox(masterSecret, incomingEndSessionMessage);
database.updateMessageBody(masterSecret, messageAndThreadId.first, messageContent.getBody());
Session.abortSessionFor(context, recipient);
KeyExchangeProcessor.broadcastSecurityUpdateEvent(context, messageAndThreadId.second);
} catch (RecipientFormattingException e) {
Log.w("PushReceiver", e);
}
}
private void handleReceivedMediaMessage(MasterSecret masterSecret, private void handleReceivedMediaMessage(MasterSecret masterSecret,
IncomingPushMessage message, IncomingPushMessage message,
PushMessageContent messageContent, PushMessageContent messageContent,

View file

@ -68,7 +68,8 @@ public class SmsReceiver {
if (WirePrefix.isEncryptedMessage(message.getMessageBody()) || if (WirePrefix.isEncryptedMessage(message.getMessageBody()) ||
WirePrefix.isKeyExchange(message.getMessageBody()) || WirePrefix.isKeyExchange(message.getMessageBody()) ||
WirePrefix.isPreKeyBundle(message.getMessageBody())) WirePrefix.isPreKeyBundle(message.getMessageBody()) ||
WirePrefix.isEndSession(message.getMessageBody()))
{ {
return multipartMessageHandler.processPotentialMultipartMessage(message); return multipartMessageHandler.processPotentialMultipartMessage(message);
} else { } else {
@ -85,7 +86,7 @@ public class SmsReceiver {
messageAndThreadId.second, messageAndThreadId.second,
message.getSender(), message.getSenderDeviceId(), message.getSender(), message.getSenderDeviceId(),
message.getMessageBody(), message.isSecureMessage(), message.getMessageBody(), message.isSecureMessage(),
message.isKeyExchange()); message.isKeyExchange(), message.isEndSession());
} }
return messageAndThreadId; return messageAndThreadId;
@ -197,10 +198,13 @@ public class SmsReceiver {
if (message.isSecureMessage()) return storeSecureMessage(masterSecret, message); if (message.isSecureMessage()) return storeSecureMessage(masterSecret, message);
else if (message.isPreKeyBundle()) return storePreKeyWhisperMessage(masterSecret, (IncomingPreKeyBundleMessage) message); else if (message.isPreKeyBundle()) return storePreKeyWhisperMessage(masterSecret, (IncomingPreKeyBundleMessage) message);
else if (message.isKeyExchange()) return storeKeyExchangeMessage(masterSecret, (IncomingKeyExchangeMessage) message); else if (message.isKeyExchange()) return storeKeyExchangeMessage(masterSecret, (IncomingKeyExchangeMessage) message);
else if (message.isEndSession()) return storeSecureMessage(masterSecret, message);
else return storeStandardMessage(masterSecret, message); else return storeStandardMessage(masterSecret, message);
} }
private void handleReceiveMessage(MasterSecret masterSecret, Intent intent) { private void handleReceiveMessage(MasterSecret masterSecret, Intent intent) {
if (intent.getExtras() == null) return;
List<IncomingTextMessage> messagesList = intent.getExtras().getParcelableArrayList("text_messages"); List<IncomingTextMessage> messagesList = intent.getExtras().getParcelableArrayList("text_messages");
IncomingTextMessage message = assembleMessageFragments(messagesList); IncomingTextMessage message = assembleMessageFragments(messagesList);

View file

@ -21,13 +21,16 @@ import android.app.AlarmManager;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.database.Cursor;
import android.telephony.SmsManager; import android.telephony.SmsManager;
import android.telephony.SmsMessage; import android.telephony.SmsMessage;
import android.util.Log; import android.util.Log;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.crypto.KeyExchangeProcessor;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.EncryptingSmsDatabase; import org.thoughtcrime.securesms.database.EncryptingSmsDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord; import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.recipients.Recipients; import org.thoughtcrime.securesms.recipients.Recipients;
@ -38,6 +41,7 @@ import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
import org.thoughtcrime.securesms.transport.UniversalTransport; import org.thoughtcrime.securesms.transport.UniversalTransport;
import org.thoughtcrime.securesms.transport.UntrustedIdentityException; import org.thoughtcrime.securesms.transport.UntrustedIdentityException;
import org.whispersystems.textsecure.crypto.MasterSecret; import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.storage.Session;
public class SmsSender { public class SmsSender {
@ -109,10 +113,22 @@ public class SmsSender {
Log.w("SMSReceiverService", "Running sent callback: " + messageId); Log.w("SMSReceiverService", "Running sent callback: " + messageId);
if (result == Activity.RESULT_OK) { if (result == Activity.RESULT_OK) {
DatabaseFactory.getSmsDatabase(context).markAsSent(messageId); SmsDatabase database = DatabaseFactory.getSmsDatabase(context);
Cursor cursor = database.getMessage(messageId);
SmsDatabase.Reader reader = database.readerFor(cursor);
database.markAsSent(messageId);
if (upgraded) { if (upgraded) {
DatabaseFactory.getSmsDatabase(context).markAsSecure(messageId); database.markAsSecure(messageId);
}
SmsMessageRecord record = reader.getNext();
if (record != null && record.isEndSession()) {
Log.w("SmsSender", "Ending session...");
Session.abortSessionFor(context, record.getIndividualRecipient());
KeyExchangeProcessor.broadcastSecurityUpdateEvent(context, record.getThreadId());
} }
unregisterForRadioChanges(); unregisterForRadioChanges();
@ -161,4 +177,5 @@ public class SmsSender {
null, context, SendReceiveService.class), null, context, SendReceiveService.class),
PendingIntent.FLAG_UPDATE_CURRENT)); PendingIntent.FLAG_UPDATE_CURRENT));
} }
} }

View file

@ -0,0 +1,22 @@
package org.thoughtcrime.securesms.sms;
public class IncomingEndSessionMessage extends IncomingTextMessage {
public IncomingEndSessionMessage(IncomingTextMessage base) {
this(base, base.getMessageBody());
}
public IncomingEndSessionMessage(IncomingTextMessage base, String newBody) {
super(base, newBody);
}
@Override
public IncomingEndSessionMessage withMessageBody(String messageBody) {
return new IncomingEndSessionMessage(this, messageBody);
}
@Override
public boolean isEndSession() {
return true;
}
}

View file

@ -216,6 +216,10 @@ public class IncomingTextMessage implements Parcelable {
return false; return false;
} }
public boolean isEndSession() {
return false;
}
public boolean isIdentityUpdate() { public boolean isIdentityUpdate() {
return false; return false;
} }

View file

@ -71,6 +71,8 @@ public class MultipartSmsMessageHandler {
return new IncomingKeyExchangeMessage(message.getBaseMessage(), strippedMessage); return new IncomingKeyExchangeMessage(message.getBaseMessage(), strippedMessage);
} else if (message.getWireType() == MultipartSmsTransportMessage.WIRETYPE_PREKEY) { } else if (message.getWireType() == MultipartSmsTransportMessage.WIRETYPE_PREKEY) {
return new IncomingPreKeyBundleMessage(message.getBaseMessage(), strippedMessage); return new IncomingPreKeyBundleMessage(message.getBaseMessage(), strippedMessage);
} else if (message.getWireType() == MultipartSmsTransportMessage.WIRETYPE_END_SESSION) {
return new IncomingEndSessionMessage(message.getBaseMessage(), strippedMessage);
} else { } else {
return new IncomingEncryptedMessage(message.getBaseMessage(), strippedMessage); return new IncomingEncryptedMessage(message.getBaseMessage(), strippedMessage);
} }

View file

@ -2,12 +2,14 @@ package org.thoughtcrime.securesms.sms;
import android.util.Log; import android.util.Log;
import org.thoughtcrime.securesms.protocol.EndSessionWirePrefix;
import org.thoughtcrime.securesms.protocol.KeyExchangeWirePrefix; import org.thoughtcrime.securesms.protocol.KeyExchangeWirePrefix;
import org.thoughtcrime.securesms.protocol.PrekeyBundleWirePrefix; import org.thoughtcrime.securesms.protocol.PrekeyBundleWirePrefix;
import org.thoughtcrime.securesms.protocol.SecureMessageWirePrefix; import org.thoughtcrime.securesms.protocol.SecureMessageWirePrefix;
import org.thoughtcrime.securesms.protocol.WirePrefix; import org.thoughtcrime.securesms.protocol.WirePrefix;
import org.whispersystems.textsecure.util.Base64; import org.whispersystems.textsecure.util.Base64;
import org.whispersystems.textsecure.util.Conversions; import org.whispersystems.textsecure.util.Conversions;
import org.whispersystems.textsecure.util.Hex;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
@ -21,9 +23,10 @@ public class MultipartSmsTransportMessage {
public static final int MULTI_MESSAGE_MULTIPART_OVERHEAD = 3; public static final int MULTI_MESSAGE_MULTIPART_OVERHEAD = 3;
public static final int FIRST_MULTI_MESSAGE_MULTIPART_OVERHEAD = 2; public static final int FIRST_MULTI_MESSAGE_MULTIPART_OVERHEAD = 2;
public static final int WIRETYPE_SECURE = 1; public static final int WIRETYPE_SECURE = 1;
public static final int WIRETYPE_KEY = 2; public static final int WIRETYPE_KEY = 2;
public static final int WIRETYPE_PREKEY = 3; public static final int WIRETYPE_PREKEY = 3;
public static final int WIRETYPE_END_SESSION = 4;
private static final int VERSION_OFFSET = 0; private static final int VERSION_OFFSET = 0;
private static final int MULTIPART_OFFSET = 1; private static final int MULTIPART_OFFSET = 1;
@ -39,6 +42,7 @@ public class MultipartSmsTransportMessage {
if (WirePrefix.isEncryptedMessage(message.getMessageBody())) wireType = WIRETYPE_SECURE; if (WirePrefix.isEncryptedMessage(message.getMessageBody())) wireType = WIRETYPE_SECURE;
else if (WirePrefix.isPreKeyBundle(message.getMessageBody())) wireType = WIRETYPE_PREKEY; else if (WirePrefix.isPreKeyBundle(message.getMessageBody())) wireType = WIRETYPE_PREKEY;
else if (WirePrefix.isEndSession(message.getMessageBody())) wireType = WIRETYPE_END_SESSION;
else wireType = WIRETYPE_KEY; else wireType = WIRETYPE_KEY;
Log.w(TAG, "Decoded message with version: " + getCurrentVersion()); Log.w(TAG, "Decoded message with version: " + getCurrentVersion());
@ -158,6 +162,7 @@ public class MultipartSmsTransportMessage {
if (message.isKeyExchange()) prefix = new KeyExchangeWirePrefix(); if (message.isKeyExchange()) prefix = new KeyExchangeWirePrefix();
else if (message.isPreKeyBundle()) prefix = new PrekeyBundleWirePrefix(); else if (message.isPreKeyBundle()) prefix = new PrekeyBundleWirePrefix();
else if (message.isEndSession()) prefix = new EndSessionWirePrefix();
else prefix = new SecureMessageWirePrefix(); else prefix = new SecureMessageWirePrefix();
if (count == 1) return getSingleEncoded(decoded, prefix); if (count == 1) return getSingleEncoded(decoded, prefix);

View file

@ -0,0 +1,22 @@
package org.thoughtcrime.securesms.sms;
public class OutgoingEndSessionMessage extends OutgoingTextMessage {
public OutgoingEndSessionMessage(OutgoingTextMessage base) {
this(base, base.getMessageBody());
}
public OutgoingEndSessionMessage(OutgoingTextMessage message, String body) {
super(message, body);
}
@Override
public boolean isEndSession() {
return true;
}
@Override
public OutgoingTextMessage withBody(String body) {
return new OutgoingEndSessionMessage(this, body);
}
}

View file

@ -52,6 +52,10 @@ public class OutgoingTextMessage {
return false; return false;
} }
public boolean isEndSession() {
return false;
}
public boolean isPreKeyBundle() { public boolean isPreKeyBundle() {
return false; return false;
} }
@ -61,6 +65,8 @@ public class OutgoingTextMessage {
return new OutgoingEncryptedMessage(record.getIndividualRecipient(), record.getBody().getBody()); return new OutgoingEncryptedMessage(record.getIndividualRecipient(), record.getBody().getBody());
} else if (record.isKeyExchange()) { } else if (record.isKeyExchange()) {
return new OutgoingKeyExchangeMessage(record.getIndividualRecipient(), record.getBody().getBody()); return new OutgoingKeyExchangeMessage(record.getIndividualRecipient(), record.getBody().getBody());
} else if (record.isEndSession()) {
return new OutgoingEndSessionMessage(new OutgoingTextMessage(record.getIndividualRecipient(), record.getBody().getBody()));
} else { } else {
return new OutgoingTextMessage(record.getIndividualRecipient(), record.getBody().getBody()); return new OutgoingTextMessage(record.getIndividualRecipient(), record.getBody().getBody());
} }

View file

@ -22,6 +22,7 @@ import android.util.Log;
import com.google.protobuf.ByteString; import com.google.protobuf.ByteString;
import org.thoughtcrime.securesms.crypto.KeyExchangeProcessor;
import org.thoughtcrime.securesms.crypto.KeyExchangeProcessorV2; import org.thoughtcrime.securesms.crypto.KeyExchangeProcessorV2;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord; import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
@ -85,12 +86,15 @@ public class PushTransport extends BaseTransport {
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);
byte[] plaintext = PushMessageContent.newBuilder() byte[] plaintext = getPlaintextMessage(message);
.setBody(message.getBody().getBody())
.build().toByteArray();
deliver(socket, recipient, threadId, plaintext); deliver(socket, recipient, threadId, plaintext);
if (message.isEndSession()) {
SessionRecordV2.deleteAll(context, recipient);
KeyExchangeProcessor.broadcastSecurityUpdateEvent(context, threadId);
}
context.sendBroadcast(constructSentIntent(context, message.getId(), message.getType(), true)); context.sendBroadcast(constructSentIntent(context, message.getId(), message.getType(), true));
} catch (InvalidNumberException e) { } catch (InvalidNumberException e) {
@ -295,6 +299,17 @@ public class PushTransport extends BaseTransport {
return builder.build().toByteArray(); return builder.build().toByteArray();
} }
private byte[] getPlaintextMessage(SmsMessageRecord record) {
PushMessageContent.Builder builder = PushMessageContent.newBuilder()
.setBody(record.getBody().getBody());
if (record.isEndSession()) {
builder.setFlags(PushMessageContent.Flags.END_SESSION_VALUE);
}
return builder.build().toByteArray();
}
private OutgoingPushMessageList getEncryptedMessages(PushServiceSocket socket, long threadId, private OutgoingPushMessageList getEncryptedMessages(PushServiceSocket socket, long threadId,
Recipient recipient, byte[] plaintext) Recipient recipient, byte[] plaintext)
throws IOException, InvalidNumberException, UntrustedIdentityException throws IOException, InvalidNumberException, UntrustedIdentityException
@ -352,8 +367,4 @@ public class PushTransport extends BaseTransport {
throw new AssertionError("Unknown ciphertext type: " + message.getType()); throw new AssertionError("Unknown ciphertext type: " + message.getType());
} }
} }
private void destroySessions(Recipient recipient) {
SessionRecordV2.deleteAll(context, recipient);
}
} }

View file

@ -33,6 +33,7 @@ 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.storage.RecipientDevice;
import org.whispersystems.textsecure.util.Hex;
import java.util.ArrayList; import java.util.ArrayList;
@ -47,7 +48,7 @@ public class SmsTransport extends BaseTransport {
} }
public void deliver(SmsMessageRecord message) throws UndeliverableMessageException { public void deliver(SmsMessageRecord message) throws UndeliverableMessageException {
if (message.isSecure() || message.isKeyExchange()) { if (message.isSecure() || message.isKeyExchange() || message.isEndSession()) {
deliverSecureMessage(message); deliverSecureMessage(message);
} else { } else {
deliverPlaintextMessage(message); deliverPlaintextMessage(message);
@ -58,7 +59,7 @@ public class SmsTransport extends BaseTransport {
MultipartSmsMessageHandler multipartMessageHandler = new MultipartSmsMessageHandler(); MultipartSmsMessageHandler multipartMessageHandler = new MultipartSmsMessageHandler();
OutgoingTextMessage transportMessage = OutgoingTextMessage.from(message); OutgoingTextMessage transportMessage = OutgoingTextMessage.from(message);
if (message.isSecure()) { if (message.isSecure() || message.isEndSession()) {
transportMessage = getAsymmetricEncrypt(masterSecret, transportMessage); transportMessage = getAsymmetricEncrypt(masterSecret, transportMessage);
} }

View file

@ -19,12 +19,12 @@ package org.thoughtcrime.securesms.util;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.graphics.Typeface; import android.graphics.Typeface;
import android.os.Build;
import android.provider.Telephony;
import android.text.Spannable; import android.text.Spannable;
import android.text.SpannableString; import android.text.SpannableString;
import android.text.style.StyleSpan; import android.text.style.StyleSpan;
import android.util.Log; import android.util.Log;
import android.os.Build;
import android.provider.Telephony;
import org.whispersystems.textsecure.util.InvalidNumberException; import org.whispersystems.textsecure.util.InvalidNumberException;
import org.whispersystems.textsecure.util.PhoneNumberFormatter; import org.whispersystems.textsecure.util.PhoneNumberFormatter;