Handle identity key mismatch on outgoing group messages.

Additionally, make the group creation process asynchronous.
This commit is contained in:
Moxie Marlinspike 2014-02-17 11:42:51 -08:00
parent 5810062b25
commit b9f4fba98a
19 changed files with 355 additions and 156 deletions

View File

@ -5,7 +5,6 @@ import android.util.Log;
import com.google.thoughtcrimegson.Gson;
import com.google.thoughtcrimegson.JsonParseException;
import com.google.thoughtcrimegson.JsonSyntaxException;
import org.apache.http.conn.ssl.StrictHostnameVerifier;
import org.whispersystems.textsecure.crypto.IdentityKey;
@ -27,7 +26,6 @@ import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
@ -94,7 +92,7 @@ public class PushServiceSocket {
try {
makeRequest(String.format(MESSAGE_PATH, bundle.getDestination()), "PUT", new Gson().toJson(bundle));
} catch (NotFoundException nfe) {
throw new UnregisteredUserException(nfe);
throw new UnregisteredUserException(bundle.getDestination(), nfe);
}
}

View File

@ -5,8 +5,14 @@ import java.util.List;
public class UnregisteredUserException extends IOException {
public UnregisteredUserException(Exception exception) {
private final String e164number;
public UnregisteredUserException(String e164number, Exception exception) {
super(exception);
this.e164number = e164number;
}
public String getE164Number() {
return e164number;
}
}

View File

@ -34,6 +34,7 @@ import android.provider.ContactsContract.QuickContact;
import org.thoughtcrime.securesms.util.DateUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.util.Pair;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
@ -42,6 +43,7 @@ import android.widget.TextView;
import android.widget.Toast;
import android.webkit.MimeTypeMap;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
@ -53,14 +55,17 @@ import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.service.SendReceiveService;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.Emoji;
import org.whispersystems.textsecure.util.Base64;
import org.whispersystems.textsecure.util.FutureTaskListener;
import org.whispersystems.textsecure.util.ListenableFutureTask;
import org.whispersystems.textsecure.util.Util;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
import static org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent.GroupContext;
@ -172,13 +177,14 @@ public class ConversationItem extends LinearLayout {
/// MessageRecord Attribute Parsers
private void setBodyText(MessageRecord messageRecord) {
// TODO jake is going to fill these in
switch (messageRecord.getGroupAction()) {
case GroupContext.Type.QUIT_VALUE:
bodyText.setText(messageRecord.getIndividualRecipient().toShortString() + " has left the group.");
return;
case GroupContext.Type.ADD_VALUE:
case GroupContext.Type.CREATE_VALUE:
bodyText.setText(messageRecord.getGroupActionArguments() + " have joined the group.");
bodyText.setText(Util.join(GroupUtil.getSerializedArgumentMembers(messageRecord.getGroupActionArguments()), ", ") + " have joined the group.");
return;
case GroupContext.Type.MODIFY_VALUE:
bodyText.setText(messageRecord.getIndividualRecipient() + " has updated the group.");

View File

@ -9,7 +9,6 @@ import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.util.Pair;
import android.view.View;
import android.widget.EditText;
import android.widget.ImageView;
@ -30,8 +29,7 @@ import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
import org.thoughtcrime.securesms.transport.PushTransport;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.util.ActionBarUtil;
import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicTheme;
@ -42,7 +40,7 @@ import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.directory.Directory;
import org.whispersystems.textsecure.directory.NotInDirectoryException;
import org.whispersystems.textsecure.push.PushAttachmentPointer;
import org.whispersystems.textsecure.util.Base64;
import org.whispersystems.textsecure.util.InvalidNumberException;
import java.io.ByteArrayOutputStream;
@ -54,8 +52,9 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import ws.com.google.android.mms.MmsException;
import static org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData;
import static org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent.AttachmentPointer;
import static org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent.GroupContext;
@ -355,59 +354,32 @@ public class GroupCreateActivity extends PassphraseRequiredSherlockFragmentActiv
}
}
private Pair<Long, List<Recipient>> handleCreatePushGroup(String groupName,
private long handleCreatePushGroup(String groupName,
byte[] avatar,
Set<Recipient> members)
throws IOException, InvalidNumberException
{
List<String> memberE164Numbers = getE164Numbers(members);
PushTransport transport = new PushTransport(this, masterSecret);
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(this);
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(this);
byte[] groupId = groupDatabase.allocateGroupId();
AttachmentPointer avatarPointer = null;
memberE164Numbers.add(TextSecurePreferences.getLocalNumber(this));
GroupContext.Builder builder = GroupContext.newBuilder()
.setId(ByteString.copyFrom(groupId))
.setType(GroupContext.Type.CREATE)
.setName(groupName)
.addAllMembers(memberE164Numbers);
if (avatar != null) {
PushAttachmentPointer pointer = transport.createAttachment("image/png", avatar);
avatarPointer = AttachmentPointer.newBuilder()
.setKey(ByteString.copyFrom(pointer.getKey()))
.setContentType(pointer.getContentType())
.setId(pointer.getId()).build();
builder.setAvatar(avatarPointer);
}
List<Recipient> failures = transport.deliver(new LinkedList<Recipient>(members), builder.build());
groupDatabase.create(groupId, TextSecurePreferences.getLocalNumber(this), groupName,
memberE164Numbers, avatarPointer, null);
if (avatar != null) {
groupDatabase.updateAvatar(groupId, avatar);
}
try {
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(this);
List<String> memberE164Numbers = getE164Numbers(members);
byte[] groupId = groupDatabase.allocateGroupId();
String groupRecipientId = GroupUtil.getEncodedId(groupId);
Recipient groupRecipient = RecipientFactory.getRecipientsFromString(this, groupRecipientId, false).getPrimaryRecipient();
OutgoingTextMessage outgoing = new OutgoingTextMessage(groupRecipient, GroupContext.Type.ADD_VALUE, org.whispersystems.textsecure.util.Util.join(memberE164Numbers, ","));
long threadId = threadDatabase.getThreadIdFor(new Recipients(groupRecipient));
List<Long> messageIds = DatabaseFactory.getEncryptingSmsDatabase(this)
.insertMessageOutbox(masterSecret, threadId, outgoing);
for (long messageId : messageIds) {
DatabaseFactory.getEncryptingSmsDatabase(this).markAsSent(messageId);
}
String groupActionArguments = GroupUtil.serializeArguments(groupId, groupName, memberE164Numbers);
groupDatabase.create(groupId, TextSecurePreferences.getLocalNumber(this), groupName,
memberE164Numbers, null, null);
groupDatabase.updateAvatar(groupId, avatar);
return new Pair<Long, List<Recipient>>(threadId, failures);
Recipients groupRecipient = RecipientFactory.getRecipientsFromString(this, groupRecipientId, false);
return MessageSender.sendGroupAction(this, masterSecret, groupRecipient, -1,
GroupContext.Type.CREATE_VALUE,
groupActionArguments, avatar);
} catch (RecipientFormattingException e) {
throw new AssertionError(e);
throw new IOException(e);
} catch (MmsException e) {
throw new IOException(e);
}
}

View File

@ -132,7 +132,8 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
CONTENT_LOCATION, EXPIRY, MESSAGE_CLASS, MESSAGE_TYPE, MMS_VERSION,
MESSAGE_SIZE, PRIORITY, REPORT_ALLOWED, STATUS, TRANSACTION_ID, RETRIEVE_STATUS,
RETRIEVE_TEXT, RETRIEVE_TEXT_CS, READ_STATUS, CONTENT_CLASS, RESPONSE_TEXT,
DELIVERY_TIME, DELIVERY_REPORT, BODY, PART_COUNT, ADDRESS, ADDRESS_DEVICE_ID
DELIVERY_TIME, DELIVERY_REPORT, BODY, PART_COUNT, ADDRESS, ADDRESS_DEVICE_ID,
GROUP_ACTION, GROUP_ACTION_ARGUMENTS
};
public static final ExecutorService slideResolver = org.thoughtcrime.securesms.util.Util.newSingleThreadedLifoExecutor();
@ -344,8 +345,11 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
while (cursor.moveToNext()) {
messageId = cursor.getLong(cursor.getColumnIndexOrThrow(ID));
long outboxType = cursor.getLong(cursor.getColumnIndexOrThrow(MESSAGE_BOX));
String messageText = cursor.getString(cursor.getColumnIndexOrThrow(BODY));
int groupAction = cursor.getInt(cursor.getColumnIndexOrThrow(GROUP_ACTION));
String groupActionArguments = cursor.getString(cursor.getColumnIndexOrThrow(GROUP_ACTION_ARGUMENTS));
PduHeaders headers = getHeadersFromCursor(cursor);
addr.getAddressesForId(messageId, headers);
@ -361,7 +365,7 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
Log.w("MmsDatabase", e);
}
requests[i++] = new SendReq(headers, body, messageId, outboxType);
requests[i++] = new SendReq(headers, body, messageId, outboxType, groupAction, groupActionArguments);
}
return requests;
@ -514,6 +518,8 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
contentValues.put(THREAD_ID, threadId);
contentValues.put(READ, 1);
contentValues.put(DATE_RECEIVED, contentValues.getAsLong(DATE_SENT));
contentValues.put(GROUP_ACTION, sendRequest.getGroupAction());
contentValues.put(GROUP_ACTION_ARGUMENTS, sendRequest.getGroupActionArguments());
contentValues.remove(ADDRESS);
long messageId = insertMediaMessage(masterSecret, sendRequest.getPduHeaders(),

View File

@ -20,13 +20,17 @@ import android.content.Context;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.style.StyleSpan;
import android.util.Pair;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.whispersystems.textsecure.push.PushMessageProtos;
import org.whispersystems.textsecure.util.Util;
import java.util.List;
import static org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent.GroupContext;
/**
@ -55,12 +59,13 @@ public class ThreadRecord extends DisplayRecord {
@Override
public SpannableString getDisplayBody() {
// TODO jake is going to fill these in
if (SmsDatabase.Types.isDecryptInProgressType(type)) {
return emphasisAdded(context.getString(R.string.MessageDisplayHelper_decrypting_please_wait));
} else if (getGroupAction() == GroupContext.Type.ADD_VALUE ||
getGroupAction() == GroupContext.Type.CREATE_VALUE)
{
return emphasisAdded("Added " + getGroupActionArguments());
return emphasisAdded(Util.join(GroupUtil.getSerializedArgumentMembers(getGroupActionArguments()), ", ") + " have joined the group");
} else if (getGroupAction() == GroupContext.Type.QUIT_VALUE) {
return emphasisAdded(getRecipients().toShortString() + " left the group.");
} else if (getGroupAction() == GroupContext.Type.MODIFY_VALUE) {

View File

@ -41,7 +41,7 @@ public class IncomingMediaMessage {
if (messageContent.hasGroup()) {
this.groupId = GroupUtil.getEncodedId(messageContent.getGroup().getId().toByteArray());
this.groupAction = messageContent.getGroup().getType().getNumber();
this.groupActionArguments = GroupUtil.getActionArgument(messageContent.getGroup());
this.groupActionArguments = GroupUtil.serializeArguments(messageContent.getGroup());
} else {
this.groupId = null;
this.groupAction = -1;

View File

@ -92,8 +92,7 @@ public class MmsSender {
Recipients recipients = threads.getRecipientsForThreadId(threadId);
MessageNotifier.notifyMessageDeliveryFailed(context, recipients, threadId);
} catch (UntrustedIdentityException uie) {
IncomingTextMessage base = new IncomingTextMessage(message);
IncomingIdentityUpdateMessage identityUpdateMessage = new IncomingIdentityUpdateMessage(base, Base64.encodeBytesWithoutPadding(uie.getIdentityKey().serialize()));
IncomingIdentityUpdateMessage identityUpdateMessage = IncomingIdentityUpdateMessage.createFor(message.getTo()[0].getString(), uie.getIdentityKey());
DatabaseFactory.getEncryptingSmsDatabase(context).insertMessageInbox(masterSecret, identityUpdateMessage);
database.markAsSentFailed(messageId);
} catch (RetryLaterException e) {

View File

@ -81,8 +81,7 @@ public class SmsSender {
transport.deliver(record);
} catch (UntrustedIdentityException e) {
Log.w("SmsSender", e);
IncomingTextMessage base = new IncomingTextMessage(record);
IncomingIdentityUpdateMessage identityUpdateMessage = new IncomingIdentityUpdateMessage(base, Base64.encodeBytesWithoutPadding(e.getIdentityKey().serialize()));
IncomingIdentityUpdateMessage identityUpdateMessage = IncomingIdentityUpdateMessage.createFor(e.getE164Number(), e.getIdentityKey());
DatabaseFactory.getEncryptingSmsDatabase(context).insertMessageInbox(masterSecret, identityUpdateMessage);
DatabaseFactory.getSmsDatabase(context).markAsSentFailed(messageId);
} catch (UndeliverableMessageException ude) {

View File

@ -1,5 +1,8 @@
package org.thoughtcrime.securesms.sms;
import org.whispersystems.textsecure.crypto.IdentityKey;
import org.whispersystems.textsecure.util.Base64;
public class IncomingIdentityUpdateMessage extends IncomingKeyExchangeMessage {
public IncomingIdentityUpdateMessage(IncomingTextMessage base, String newBody) {
@ -15,4 +18,13 @@ public class IncomingIdentityUpdateMessage extends IncomingKeyExchangeMessage {
public boolean isIdentityUpdate() {
return true;
}
public static IncomingIdentityUpdateMessage createFor(String sender, IdentityKey identityKey) {
return createFor(sender, identityKey, null);
}
public static IncomingIdentityUpdateMessage createFor(String sender, IdentityKey identityKey, String groupId) {
IncomingTextMessage base = new IncomingTextMessage(sender, groupId, -1, null);
return new IncomingIdentityUpdateMessage(base, Base64.encodeBytesWithoutPadding(identityKey.serialize()));
}
}

View File

@ -68,7 +68,7 @@ public class IncomingTextMessage implements Parcelable {
if (group != null) {
this.groupId = GroupUtil.getEncodedId(group.getId().toByteArray());
this.groupAction = group.getType().getNumber();
this.groupActionArgument = GroupUtil.getActionArgument(group);
this.groupActionArgument = GroupUtil.serializeArguments(group);
} else {
this.groupId = null;
this.groupAction = -1;
@ -152,6 +152,22 @@ public class IncomingTextMessage implements Parcelable {
this.groupActionArgument = null;
}
protected IncomingTextMessage(String sender, String groupId,
int groupAction, String groupActionArgument)
{
this.message = "";
this.sender = sender;
this.senderDeviceId = RecipientDevice.DEFAULT_DEVICE_ID;
this.protocol = 31338;
this.serviceCenterAddress = "Outgoing";
this.replyPathPresent = true;
this.pseudoSubject = "";
this.sentTimestampMillis = System.currentTimeMillis();
this.groupId = groupId;
this.groupAction = groupAction;
this.groupActionArgument = groupActionArgument;
}
public long getSentTimestampMillis() {
return sentTimestampMillis;
}
@ -235,4 +251,8 @@ public class IncomingTextMessage implements Parcelable {
out.writeInt(groupAction);
out.writeString(groupActionArgument);
}
public static IncomingTextMessage createForLeavingGroup(String groupId, String user) {
return new IncomingTextMessage(user, groupId, GroupContext.Type.QUIT_VALUE, null);
}
}

View File

@ -20,6 +20,7 @@ import android.content.Context;
import android.content.Intent;
import android.util.Log;
import org.thoughtcrime.securesms.mms.ImageSlide;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.ThreadDatabase;
@ -34,10 +35,43 @@ import ws.com.google.android.mms.ContentType;
import ws.com.google.android.mms.MmsException;
import ws.com.google.android.mms.pdu.EncodedStringValue;
import ws.com.google.android.mms.pdu.PduBody;
import ws.com.google.android.mms.pdu.PduPart;
import ws.com.google.android.mms.pdu.SendReq;
public class MessageSender {
public static long sendGroupAction(Context context, MasterSecret masterSecret, Recipients recipients,
long threadId, int groupAction, String groupActionArguments, byte[] avatar)
throws MmsException
{
if (threadId == -1) {
threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipients);
}
PduBody body = new PduBody();
if (avatar != null) {
PduPart part = new PduPart();
part.setData(avatar);
part.setContentType(ContentType.IMAGE_PNG.getBytes());
part.setContentId((System.currentTimeMillis()+"").getBytes());
part.setName(("Image" + System.currentTimeMillis()).getBytes());
body.addPart(part);
}
SendReq sendRequest = new SendReq();
sendRequest.setDate(System.currentTimeMillis() / 1000L);
sendRequest.setBody(body);
sendRequest.setContentType(ContentType.MULTIPART_MIXED.getBytes());
sendRequest.setGroupAction(groupAction);
sendRequest.setGroupActionArguments(groupActionArguments);
sendMms(context, recipients, masterSecret, sendRequest, threadId,
ThreadDatabase.DistributionTypes.CONVERSATION, true);
return threadId;
}
public static long sendMms(Context context, MasterSecret masterSecret, Recipients recipients,
long threadId, SlideDeck slideDeck, String message, int distributionType,
boolean secure)

View File

@ -0,0 +1,26 @@
package org.thoughtcrime.securesms.transport;
import org.whispersystems.textsecure.push.UnregisteredUserException;
import java.util.List;
public class EncapsulatedExceptions extends Throwable {
private final List<UntrustedIdentityException> untrustedIdentityExceptions;
private final List<UnregisteredUserException> unregisteredUserExceptions;
public EncapsulatedExceptions(List<UntrustedIdentityException> untrustedIdentities,
List<UnregisteredUserException> unregisteredUsers)
{
this.untrustedIdentityExceptions = untrustedIdentities;
this.unregisteredUserExceptions = unregisteredUsers;
}
public List<UntrustedIdentityException> getUntrustedIdentityExceptions() {
return untrustedIdentityExceptions;
}
public List<UnregisteredUserException> getUnregisteredUserExceptions() {
return unregisteredUserExceptions;
}
}

View File

@ -51,6 +51,7 @@ import org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent;
import org.whispersystems.textsecure.push.PushServiceSocket;
import org.whispersystems.textsecure.push.UnregisteredUserException;
import org.whispersystems.textsecure.storage.SessionRecordV2;
import org.whispersystems.textsecure.util.Base64;
import org.whispersystems.textsecure.util.InvalidNumberException;
import java.io.IOException;
@ -62,6 +63,8 @@ import ws.com.google.android.mms.pdu.PduBody;
import ws.com.google.android.mms.pdu.SendReq;
import static org.whispersystems.textsecure.push.PushMessageProtos.IncomingPushMessageSignal;
import static org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent.AttachmentPointer;
import static org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent.GroupContext;
public class PushTransport extends BaseTransport {
@ -100,7 +103,7 @@ public class PushTransport extends BaseTransport {
}
public void deliver(SendReq message, long threadId)
throws IOException, UntrustedIdentityException
throws IOException, RecipientFormattingException, InvalidNumberException, EncapsulatedExceptions
{
PushServiceSocket socket = PushServiceSocketFactory.create(context);
byte[] plaintext = getPlaintextMessage(socket, message);
@ -108,7 +111,6 @@ public class PushTransport extends BaseTransport {
Recipients recipients;
try {
if (GroupUtil.isEncodedGroup(destination)) {
recipients = DatabaseFactory.getGroupDatabase(context)
.getGroupMembers(GroupUtil.getDecodedId(destination));
@ -116,59 +118,24 @@ public class PushTransport extends BaseTransport {
recipients = RecipientFactory.getRecipientsFromString(context, destination, false);
}
List<UntrustedIdentityException> untrustedIdentities = new LinkedList<UntrustedIdentityException>();
List<UnregisteredUserException> unregisteredUsers = new LinkedList<UnregisteredUserException>();
for (Recipient recipient : recipients.getRecipientsList()) {
deliver(socket, recipient, threadId, plaintext);
}
} catch (UnregisteredUserException uue) {
// TODO: We should probably remove the user from the directory?
throw new IOException(uue);
} catch (RecipientFormattingException e) {
throw new IOException(e);
} catch (InvalidNumberException e) {
throw new IOException(e);
}
}
public List<Recipient> deliver(List<Recipient> recipients,
PushMessageContent.GroupContext groupAction)
throws IOException
{
PushServiceSocket socket = PushServiceSocketFactory.create(context);
byte[] plaintext = PushMessageContent.newBuilder()
.setGroup(groupAction)
.build().toByteArray();
List<Recipient> failures = new LinkedList<Recipient>();
for (Recipient recipient : recipients) {
try {
deliver(socket, recipient, -1, plaintext);
} catch (UnregisteredUserException e) {
Log.w("PushTransport", e);
failures.add(recipient);
} catch (InvalidNumberException e) {
Log.w("PushTransport", e);
failures.add(recipient);
} catch (IOException e) {
Log.w("PushTransport", e);
failures.add(recipient);
deliver(socket, recipient, threadId, plaintext);
} catch (UntrustedIdentityException e) {
Log.w("PushTransport", e);
failures.add(recipient);
untrustedIdentities.add(e);
} catch (UnregisteredUserException e) {
Log.w("PushTransport", e);
unregisteredUsers.add(e);
}
}
if (failures.size() == recipients.size()) {
throw new IOException("Total failure.");
if (!untrustedIdentities.isEmpty() || !unregisteredUsers.isEmpty()) {
throw new EncapsulatedExceptions(untrustedIdentities, unregisteredUsers);
}
return failures;
}
public PushAttachmentPointer createAttachment(String contentType, byte[] data)
throws IOException
{
PushServiceSocket socket = PushServiceSocketFactory.create(context);
return getPushAttachmentPointer(socket, contentType, data);
}
private void deliver(PushServiceSocket socket, Recipient recipient, long threadId, byte[] plaintext)
@ -252,11 +219,40 @@ public class PushTransport extends BaseTransport {
PushMessageContent.Builder builder = PushMessageContent.newBuilder();
if (GroupUtil.isEncodedGroup(message.getTo()[0].getString())) {
PushMessageContent.GroupContext.Builder groupBuilder =
PushMessageContent.GroupContext.newBuilder();
byte[] groupId = GroupUtil.getDecodedId(message.getTo()[0].getString());
GroupContext.Builder groupBuilder = GroupContext.newBuilder();
groupBuilder.setType(PushMessageContent.GroupContext.Type.DELIVER);
groupBuilder.setId(ByteString.copyFrom(GroupUtil.getDecodedId(message.getTo()[0].getString())));
groupBuilder.setId(ByteString.copyFrom(groupId));
switch (message.getGroupAction()) {
case GroupContext.Type.ADD_VALUE: groupBuilder.setType(GroupContext.Type.ADD); break;
case GroupContext.Type.CREATE_VALUE: groupBuilder.setType(GroupContext.Type.CREATE); break;
case GroupContext.Type.QUIT_VALUE: groupBuilder.setType(GroupContext.Type.QUIT); break;
default: groupBuilder.setType(GroupContext.Type.DELIVER); break;
}
if (message.getGroupAction() == GroupContext.Type.ADD_VALUE ||
message.getGroupAction() == GroupContext.Type.CREATE_VALUE)
{
GroupContext serialized = GroupContext.parseFrom(Base64.decode(message.getGroupActionArguments()));
groupBuilder.addAllMembers(serialized.getMembersList());
if (serialized.hasName()) {
groupBuilder.setName(serialized.getName());
}
}
if (message.getGroupAction() == GroupContext.Type.CREATE_VALUE && !attachments.isEmpty()) {
Log.w("PushTransport", "Adding avatar...");
groupBuilder.setAvatar(AttachmentPointer.newBuilder()
.setId(attachments.get(0).getId())
.setContentType(attachments.get(0).getContentType())
.setKey(ByteString.copyFrom(attachments.get(0).getKey()))
.build());
attachments.remove(0);
} else {
Log.w("PushTransport", "Not adding avatar: " + message.getGroupAction() + " , " + attachments.isEmpty());
}
builder.setGroup(groupBuilder.build());
}
@ -266,8 +262,8 @@ public class PushTransport extends BaseTransport {
}
for (PushAttachmentPointer attachment : attachments) {
PushMessageContent.AttachmentPointer.Builder attachmentBuilder =
PushMessageContent.AttachmentPointer.newBuilder();
AttachmentPointer.Builder attachmentBuilder =
AttachmentPointer.newBuilder();
attachmentBuilder.setId(attachment.getId());
attachmentBuilder.setContentType(attachment.getContentType());
@ -316,7 +312,7 @@ public class PushTransport extends BaseTransport {
if (processor.isTrusted(preKey)) {
processor.processKeyExchangeMessage(preKey, threadId);
} else {
throw new UntrustedIdentityException("Untrusted identity key!", preKey.getIdentityKey());
throw new UntrustedIdentityException("Untrusted identity key!", pushAddress.getNumber(), preKey.getIdentityKey());
}
}
} catch (InvalidKeyException e) {

View File

@ -1,4 +1,9 @@
package org.thoughtcrime.securesms.transport;
import java.io.IOException;
public class RetryLaterException extends Exception {
public RetryLaterException(Exception e) {
super(e);
}
}

View File

@ -19,10 +19,14 @@ package org.thoughtcrime.securesms.transport;
import android.content.Context;
import android.util.Log;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.mms.MmsSendResult;
import org.thoughtcrime.securesms.push.PushServiceSocketFactory;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.sms.IncomingIdentityUpdateMessage;
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
@ -31,7 +35,11 @@ import org.whispersystems.textsecure.directory.Directory;
import org.whispersystems.textsecure.directory.NotInDirectoryException;
import org.whispersystems.textsecure.push.ContactNumberDetails;
import org.whispersystems.textsecure.push.ContactTokenDetails;
import org.whispersystems.textsecure.push.IncomingPushMessage;
import org.whispersystems.textsecure.push.PushMessageProtos;
import org.whispersystems.textsecure.push.PushServiceSocket;
import org.whispersystems.textsecure.push.UnregisteredUserException;
import org.whispersystems.textsecure.storage.RecipientDevice;
import org.whispersystems.textsecure.util.DirectoryUtil;
import org.whispersystems.textsecure.util.InvalidNumberException;
@ -39,15 +47,19 @@ import java.io.IOException;
import ws.com.google.android.mms.pdu.SendReq;
import static org.whispersystems.textsecure.push.PushMessageProtos.IncomingPushMessageSignal;
public class UniversalTransport {
private final Context context;
private final MasterSecret masterSecret;
private final PushTransport pushTransport;
private final SmsTransport smsTransport;
private final MmsTransport mmsTransport;
public UniversalTransport(Context context, MasterSecret masterSecret) {
this.context = context;
this.masterSecret = masterSecret;
this.pushTransport = new PushTransport(context, masterSecret);
this.smsTransport = new SmsTransport(context, masterSecret);
this.mmsTransport = new MmsTransport(context, masterSecret);
@ -90,6 +102,10 @@ public class UniversalTransport {
throw new UndeliverableMessageException("No destination specified");
}
if (GroupUtil.isEncodedGroup(mediaMessage.getTo()[0].getString())) {
return deliverGroupMessage(mediaMessage, threadId);
}
if (!TextSecurePreferences.isPushRegistered(context)) {
return mmsTransport.deliver(mediaMessage);
}
@ -98,14 +114,8 @@ public class UniversalTransport {
return mmsTransport.deliver(mediaMessage);
}
String destination;
try {
destination = Util.canonicalizeNumber(context, mediaMessage.getTo()[0].getString());
} catch (InvalidNumberException ine) {
Log.w("UniversalTransport", ine);
return mmsTransport.deliver(mediaMessage);
}
String destination = Util.canonicalizeNumber(context, mediaMessage.getTo()[0].getString());
if (isPushTransport(destination)) {
try {
@ -114,17 +124,66 @@ public class UniversalTransport {
return new MmsSendResult("push".getBytes("UTF-8"), 0, true);
} catch (IOException ioe) {
Log.w("UniversalTransport", ioe);
if (!GroupUtil.isEncodedGroup(destination)) {
return mmsTransport.deliver(mediaMessage);
} catch (RecipientFormattingException e) {
Log.w("UniversalTransport", e);
return mmsTransport.deliver(mediaMessage);
} catch (EncapsulatedExceptions ee) {
Log.w("UniversalTransport", ee);
if (!ee.getUnregisteredUserExceptions().isEmpty()) {
return mmsTransport.deliver(mediaMessage);
} else {
throw new RetryLaterException();
throw new UntrustedIdentityException(ee.getUntrustedIdentityExceptions().get(0));
}
}
} else {
Log.w("UniversalTransport", "Delivering media message with MMS...");
return mmsTransport.deliver(mediaMessage);
}
} catch (InvalidNumberException ine) {
Log.w("UniversalTransport", ine);
return mmsTransport.deliver(mediaMessage);
}
}
private MmsSendResult deliverGroupMessage(SendReq mediaMessage, long threadId)
throws RetryLaterException, UndeliverableMessageException
{
if (!TextSecurePreferences.isPushRegistered(context)) {
throw new UndeliverableMessageException("Not push registered!");
}
try {
pushTransport.deliver(mediaMessage, threadId);
return new MmsSendResult("push".getBytes("UTF-8"), 0, true);
} catch (IOException e) {
Log.w("UniversalTransport", e);
throw new RetryLaterException(e);
} catch (RecipientFormattingException e) {
throw new UndeliverableMessageException(e);
} catch (InvalidNumberException e) {
throw new UndeliverableMessageException(e);
} catch (EncapsulatedExceptions ee) {
Log.w("UniversalTransport", ee);
try {
for (UnregisteredUserException unregistered : ee.getUnregisteredUserExceptions()) {
IncomingTextMessage quitMessage = IncomingTextMessage.createForLeavingGroup(mediaMessage.getTo()[0].getString(), unregistered.getE164Number());
DatabaseFactory.getEncryptingSmsDatabase(context).insertMessageInbox(masterSecret, quitMessage);
DatabaseFactory.getGroupDatabase(context).remove(GroupUtil.getDecodedId(mediaMessage.getTo()[0].getString()), unregistered.getE164Number());
}
for (UntrustedIdentityException untrusted : ee.getUntrustedIdentityExceptions()) {
IncomingIdentityUpdateMessage identityMessage = IncomingIdentityUpdateMessage.createFor(untrusted.getE164Number(), untrusted.getIdentityKey(), mediaMessage.getTo()[0].getString());
DatabaseFactory.getEncryptingSmsDatabase(context).insertMessageInbox(masterSecret, identityMessage);
}
return new MmsSendResult("push".getBytes("UTF-8"), 0, true);
} catch (IOException ioe) {
throw new AssertionError(ioe);
}
}
}
public boolean isMultipleRecipients(SendReq mediaMessage) {
int recipientCount = 0;

View File

@ -1,17 +1,29 @@
package org.thoughtcrime.securesms.transport;
import org.whispersystems.textsecure.crypto.IdentityKey;
import org.whispersystems.textsecure.push.UnregisteredUserException;
public class UntrustedIdentityException extends Exception {
private final IdentityKey identityKey;
private final String e164number;
public UntrustedIdentityException(String s, IdentityKey identityKey) {
public UntrustedIdentityException(String s, String e164number, IdentityKey identityKey) {
super(s);
this.e164number = e164number;
this.identityKey = identityKey;
}
public UntrustedIdentityException(UntrustedIdentityException e) {
this(e.getMessage(), e.getE164Number(), e.getIdentityKey());
}
public IdentityKey getIdentityKey() {
return identityKey;
}
public String getE164Number() {
return e164number;
}
}

View File

@ -1,8 +1,16 @@
package org.thoughtcrime.securesms.util;
import android.util.Log;
import android.util.Pair;
import com.google.protobuf.ByteString;
import org.whispersystems.textsecure.util.Base64;
import org.whispersystems.textsecure.util.Hex;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import static org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent.GroupContext;
@ -26,15 +34,29 @@ public class GroupUtil {
return groupId.startsWith(ENCODED_GROUP_PREFIX);
}
public static String getActionArgument(GroupContext group) {
if (group.getType().equals(GroupContext.Type.CREATE) ||
group.getType().equals(GroupContext.Type.ADD))
{
return org.whispersystems.textsecure.util.Util.join(group.getMembersList(), ",");
} else if (group.getType().equals(GroupContext.Type.MODIFY)) {
return group.getName();
public static String serializeArguments(byte[] id, String name, List<String> members) {
return Base64.encodeBytes(GroupContext.newBuilder()
.setId(ByteString.copyFrom(id))
.setName(name)
.addAllMembers(members)
.build().toByteArray());
}
return null;
public static String serializeArguments(GroupContext context) {
return Base64.encodeBytes(context.toByteArray());
}
public static List<String> getSerializedArgumentMembers(String serialized) {
if (serialized == null) {
return new LinkedList<String>();
}
try {
GroupContext context = GroupContext.parseFrom(Base64.decode(serialized));
return context.getMembersList();
} catch (IOException e) {
Log.w("GroupUtil", e);
return new LinkedList<String>();
}
}
}

View File

@ -25,6 +25,8 @@ public class SendReq extends MultimediaMessagePdu {
private static final String TAG = "SendReq";
private long databaseMessageId;
private long messageBox;
private int groupAction;
private String groupActionArguments;
public SendReq() {
super();
@ -90,10 +92,14 @@ public class SendReq extends MultimediaMessagePdu {
super(headers, body);
}
public SendReq(PduHeaders headers, PduBody body, long messageId, long messageBox) {
public SendReq(PduHeaders headers, PduBody body, long messageId, long messageBox,
int groupAction, String groupActionArguments)
{
super(headers, body);
this.databaseMessageId = messageId;
this.messageBox = messageBox;
this.groupAction = groupAction;
this.groupActionArguments = groupActionArguments;
}
public long getDatabaseMessageBox() {
@ -104,6 +110,22 @@ public class SendReq extends MultimediaMessagePdu {
return databaseMessageId;
}
public int getGroupAction() {
return this.groupAction;
}
public String getGroupActionArguments() {
return this.groupActionArguments;
}
public void setGroupAction(int groupAction) {
this.groupAction = groupAction;
}
public void setGroupActionArguments(String groupActionArguments) {
this.groupActionArguments = groupActionArguments;
}
/**
* Get Bcc value.
*