Moxie Marlinspike 926d3c929f Handle simultaneous initiate protocol case.
1) Modify SessionRecord to store a list of "previous" sessions
   in addition to the current active session.  Previous sessions
   can be used for receiving messages, but not for sending
   messages.

2) When a possible "simultaneous initiate" is detected, push the
   current session onto the "previous session" stack instead of
   clearing it and starting over.

3) Additionally, mark the new session created on a received
   possible "simultaneous initiate" as stale for sending.  The
   next outgoing message would trigger a full prekey refresh.

4) Work to do: outgoing messages on the SMS transport should
   probably not use the existing session if it's marked stale
   for sending.  These messages need to fail and notify the user,
   similar to how we'll handle SMS fallback to push users before
   a prekey session is created.
2014-04-02 22:10:50 -07:00

364 lines
16 KiB
Java

/**
* Copyright (C) 2013 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.transport;
import android.content.Context;
import android.util.Log;
import com.google.protobuf.ByteString;
import org.thoughtcrime.securesms.crypto.KeyExchangeProcessor;
import org.thoughtcrime.securesms.crypto.KeyExchangeProcessorV2;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsSmsColumns;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.mms.PartParser;
import org.thoughtcrime.securesms.push.PushServiceSocketFactory;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.textsecure.crypto.AttachmentCipher;
import org.whispersystems.textsecure.crypto.InvalidKeyException;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.crypto.SessionCipher;
import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage;
import org.whispersystems.textsecure.push.MismatchedDevices;
import org.whispersystems.textsecure.push.MismatchedDevicesException;
import org.whispersystems.textsecure.push.OutgoingPushMessage;
import org.whispersystems.textsecure.push.OutgoingPushMessageList;
import org.whispersystems.textsecure.push.PreKeyEntity;
import org.whispersystems.textsecure.push.PushAddress;
import org.whispersystems.textsecure.push.PushAttachmentData;
import org.whispersystems.textsecure.push.PushAttachmentPointer;
import org.whispersystems.textsecure.push.PushBody;
import org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent;
import org.whispersystems.textsecure.push.PushServiceSocket;
import org.whispersystems.textsecure.push.StaleDevices;
import org.whispersystems.textsecure.push.StaleDevicesException;
import org.whispersystems.textsecure.push.UnregisteredUserException;
import org.whispersystems.textsecure.storage.SessionRecordV2;
import org.whispersystems.textsecure.util.Base64;
import org.whispersystems.textsecure.util.InvalidNumberException;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import ws.com.google.android.mms.ContentType;
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 {
private final Context context;
private final MasterSecret masterSecret;
public PushTransport(Context context, MasterSecret masterSecret) {
this.context = context.getApplicationContext();
this.masterSecret = masterSecret;
}
public void deliver(SmsMessageRecord message)
throws IOException, UntrustedIdentityException
{
try {
Recipient recipient = message.getIndividualRecipient();
long threadId = message.getThreadId();
PushServiceSocket socket = PushServiceSocketFactory.create(context);
byte[] plaintext = getPlaintextMessage(message);
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, true));
} catch (InvalidNumberException e) {
Log.w("PushTransport", e);
throw new IOException("Badly formatted number.");
}
}
public void deliver(SendReq message, long threadId)
throws IOException, RecipientFormattingException, InvalidNumberException, EncapsulatedExceptions
{
PushServiceSocket socket = PushServiceSocketFactory.create(context);
byte[] plaintext = getPlaintextMessage(socket, message);
String destination = message.getTo()[0].getString();
Recipients recipients;
if (GroupUtil.isEncodedGroup(destination)) {
recipients = DatabaseFactory.getGroupDatabase(context)
.getGroupMembers(GroupUtil.getDecodedId(destination), false);
} else {
recipients = RecipientFactory.getRecipientsFromString(context, destination, false);
}
List<UntrustedIdentityException> untrustedIdentities = new LinkedList<UntrustedIdentityException>();
List<UnregisteredUserException> unregisteredUsers = new LinkedList<UnregisteredUserException>();
for (Recipient recipient : recipients.getRecipientsList()) {
try {
deliver(socket, recipient, threadId, plaintext);
} catch (UntrustedIdentityException e) {
Log.w("PushTransport", e);
untrustedIdentities.add(e);
} catch (UnregisteredUserException e) {
Log.w("PushTransport", e);
unregisteredUsers.add(e);
}
}
if (!untrustedIdentities.isEmpty() || !unregisteredUsers.isEmpty()) {
throw new EncapsulatedExceptions(untrustedIdentities, unregisteredUsers);
}
}
private void deliver(PushServiceSocket socket, Recipient recipient, long threadId, byte[] plaintext)
throws IOException, InvalidNumberException, UntrustedIdentityException
{
for (int i=0;i<3;i++) {
try {
OutgoingPushMessageList messages = getEncryptedMessages(socket, threadId,
recipient, plaintext);
socket.sendMessage(messages);
return;
} catch (MismatchedDevicesException mde) {
Log.w("PushTransport", mde);
handleMismatchedDevices(socket, threadId, recipient, mde.getMismatchedDevices());
} catch (StaleDevicesException ste) {
Log.w("PushTransport", ste);
handleStaleDevices(recipient, ste.getStaleDevices());
}
}
}
private List<PushAttachmentPointer> getPushAttachmentPointers(PushServiceSocket socket, PduBody body)
throws IOException
{
List<PushAttachmentPointer> attachments = new LinkedList<PushAttachmentPointer>();
for (int i=0;i<body.getPartsNum();i++) {
String contentType = Util.toIsoString(body.getPart(i).getContentType());
if (ContentType.isImageType(contentType) ||
ContentType.isAudioType(contentType) ||
ContentType.isVideoType(contentType))
{
attachments.add(getPushAttachmentPointer(socket, contentType, body.getPart(i).getData()));
}
}
return attachments;
}
private PushAttachmentPointer getPushAttachmentPointer(PushServiceSocket socket,
String contentType, byte[] data)
throws IOException
{
AttachmentCipher cipher = new AttachmentCipher();
byte[] key = cipher.getCombinedKeyMaterial();
byte[] ciphertextAttachment = cipher.encrypt(data);
PushAttachmentData attachmentData = new PushAttachmentData(contentType, ciphertextAttachment);
long attachmentId = socket.sendAttachment(attachmentData);
return new PushAttachmentPointer(contentType, attachmentId, key);
}
private void handleMismatchedDevices(PushServiceSocket socket, long threadId,
Recipient recipient,
MismatchedDevices mismatchedDevices)
throws InvalidNumberException, IOException, UntrustedIdentityException
{
try {
String e164number = Util.canonicalizeNumber(context, recipient.getNumber());
long recipientId = recipient.getRecipientId();
for (int extraDeviceId : mismatchedDevices.getExtraDevices()) {
PushAddress address = PushAddress.create(context, recipientId, e164number, extraDeviceId);
SessionRecordV2.delete(context, address);
}
for (int missingDeviceId : mismatchedDevices.getMissingDevices()) {
PushAddress address = PushAddress.create(context, recipientId, e164number, missingDeviceId);
PreKeyEntity preKey = socket.getPreKey(address);
KeyExchangeProcessorV2 processor = new KeyExchangeProcessorV2(context, masterSecret, address);
if (processor.isTrusted(preKey)) {
processor.processKeyExchangeMessage(preKey, threadId);
} else {
throw new UntrustedIdentityException("Untrusted identity key!", e164number, preKey.getIdentityKey());
}
}
} catch (InvalidKeyException e) {
throw new IOException(e);
}
}
private void handleStaleDevices(Recipient recipient, StaleDevices staleDevices)
throws IOException
{
try {
long recipientId = recipient.getRecipientId();
String e164number = Util.canonicalizeNumber(context, recipient.getNumber());
for (int staleDeviceId : staleDevices.getStaleDevices()) {
PushAddress address = PushAddress.create(context, recipientId, e164number, staleDeviceId);
SessionRecordV2.delete(context, address);
}
} catch (InvalidNumberException e) {
throw new IOException(e);
}
}
private byte[] getPlaintextMessage(PushServiceSocket socket, SendReq message) throws IOException {
String messageBody = PartParser.getMessageText(message.getBody());
List<PushAttachmentPointer> attachments = getPushAttachmentPointers(socket, message.getBody());
PushMessageContent.Builder builder = PushMessageContent.newBuilder();
if (GroupUtil.isEncodedGroup(message.getTo()[0].getString())) {
GroupContext.Builder groupBuilder = GroupContext.newBuilder();
byte[] groupId = GroupUtil.getDecodedId(message.getTo()[0].getString());
groupBuilder.setId(ByteString.copyFrom(groupId));
groupBuilder.setType(GroupContext.Type.DELIVER);
if (MmsSmsColumns.Types.isGroupUpdate(message.getDatabaseMessageBox()) ||
MmsSmsColumns.Types.isGroupQuit(message.getDatabaseMessageBox()))
{
if (messageBody != null && messageBody.trim().length() > 0) {
groupBuilder = GroupContext.parseFrom(Base64.decode(messageBody)).toBuilder();
messageBody = null;
if (attachments != null && !attachments.isEmpty()) {
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);
}
}
}
builder.setGroup(groupBuilder.build());
}
if (messageBody != null) {
builder.setBody(messageBody);
}
for (PushAttachmentPointer attachment : attachments) {
AttachmentPointer.Builder attachmentBuilder =
AttachmentPointer.newBuilder();
attachmentBuilder.setId(attachment.getId());
attachmentBuilder.setContentType(attachment.getContentType());
attachmentBuilder.setKey(ByteString.copyFrom(attachment.getKey()));
builder.addAttachments(attachmentBuilder.build());
}
return builder.build().toByteArray();
}
private 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,
Recipient recipient, byte[] plaintext)
throws IOException, InvalidNumberException, UntrustedIdentityException
{
String e164number = Util.canonicalizeNumber(context, recipient.getNumber());
long recipientId = recipient.getRecipientId();
PushAddress masterDevice = PushAddress.create(context, recipientId, e164number, 1);
PushBody masterBody = getEncryptedMessage(socket, threadId, masterDevice, plaintext);
List<OutgoingPushMessage> messages = new LinkedList<OutgoingPushMessage>();
messages.add(new OutgoingPushMessage(masterDevice, masterBody));
for (int deviceId : SessionRecordV2.getSessionSubDevices(context, recipient)) {
PushAddress device = PushAddress.create(context, recipientId, e164number, deviceId);
PushBody body = getEncryptedMessage(socket, threadId, device, plaintext);
messages.add(new OutgoingPushMessage(device, body));
}
return new OutgoingPushMessageList(e164number, masterDevice.getRelay(), messages);
}
private PushBody getEncryptedMessage(PushServiceSocket socket, long threadId,
PushAddress pushAddress, byte[] plaintext)
throws IOException, UntrustedIdentityException
{
if (!SessionRecordV2.hasSession(context, masterSecret, pushAddress) ||
SessionRecordV2.needsRefresh(context, masterSecret, pushAddress))
{
try {
List<PreKeyEntity> preKeys = socket.getPreKeys(pushAddress);
for (PreKeyEntity preKey : preKeys) {
PushAddress device = PushAddress.create(context, pushAddress.getRecipientId(), pushAddress.getNumber(), preKey.getDeviceId());
KeyExchangeProcessorV2 processor = new KeyExchangeProcessorV2(context, masterSecret, device);
if (processor.isTrusted(preKey)) {
processor.processKeyExchangeMessage(preKey, threadId);
} else {
throw new UntrustedIdentityException("Untrusted identity key!", pushAddress.getNumber(), preKey.getIdentityKey());
}
}
} catch (InvalidKeyException e) {
throw new IOException(e);
}
}
SessionCipher cipher = SessionCipher.createFor(context, masterSecret, pushAddress);
CiphertextMessage message = cipher.encrypt(plaintext);
int remoteRegistrationId = cipher.getRemoteRegistrationId();
if (message.getType() == CiphertextMessage.PREKEY_WHISPER_TYPE) {
return new PushBody(IncomingPushMessageSignal.Type.PREKEY_BUNDLE_VALUE, remoteRegistrationId, message.serialize());
} else if (message.getType() == CiphertextMessage.CURRENT_WHISPER_TYPE) {
return new PushBody(IncomingPushMessageSignal.Type.CIPHERTEXT_VALUE, remoteRegistrationId, message.serialize());
} else {
throw new AssertionError("Unknown ciphertext type: " + message.getType());
}
}
}