/**
* 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 .
*/
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.TextSecureCipher;
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.libaxolotl.InvalidKeyException;
import org.whispersystems.libaxolotl.protocol.CiphertextMessage;
import org.whispersystems.libaxolotl.state.PreKeyBundle;
import org.whispersystems.libaxolotl.state.SessionStore;
import org.whispersystems.textsecure.crypto.AttachmentCipher;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.crypto.TransportDetails;
import org.whispersystems.textsecure.push.MismatchedDevices;
import org.whispersystems.textsecure.push.exceptions.MismatchedDevicesException;
import org.whispersystems.textsecure.push.OutgoingPushMessage;
import org.whispersystems.textsecure.push.OutgoingPushMessageList;
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.PushTransportDetails;
import org.whispersystems.textsecure.push.StaleDevices;
import org.whispersystems.textsecure.push.exceptions.StaleDevicesException;
import org.whispersystems.textsecure.push.UnregisteredUserException;
import org.whispersystems.textsecure.storage.SessionUtil;
import org.whispersystems.textsecure.storage.TextSecureSessionStore;
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, message.getDateSent(), threadId, plaintext);
if (message.isEndSession()) {
SessionStore sessionStore = new TextSecureSessionStore(context, masterSecret);
sessionStore.deleteAllSessions(recipient.getRecipientId());
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 untrustedIdentities = new LinkedList<>();
List unregisteredUsers = new LinkedList<>();
for (Recipient recipient : recipients.getRecipientsList()) {
try {
deliver(socket, recipient, message.getSentTimestamp(), 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 timestamp,
long threadId, byte[] plaintext)
throws IOException, InvalidNumberException, UntrustedIdentityException
{
for (int i=0;i<3;i++) {
try {
OutgoingPushMessageList messages = getEncryptedMessages(socket, threadId, recipient,
timestamp, 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 getPushAttachmentPointers(PushServiceSocket socket, PduBody body)
throws IOException
{
List attachments = new LinkedList<>();
for (int i=0;i 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, long timestamp,
byte[] plaintext)
throws IOException, InvalidNumberException, UntrustedIdentityException
{
SessionStore sessionStore = new TextSecureSessionStore(context, masterSecret);
String e164number = Util.canonicalizeNumber(context, recipient.getNumber());
long recipientId = recipient.getRecipientId();
PushAddress masterDevice = PushAddress.create(context, recipientId, e164number, 1);
PushBody masterBody = getEncryptedMessage(socket, threadId, masterDevice, plaintext);
List messages = new LinkedList<>();
messages.add(new OutgoingPushMessage(masterDevice, masterBody));
for (int deviceId : sessionStore.getSubDeviceSessions(recipientId)) {
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, timestamp, masterDevice.getRelay(), messages);
}
private PushBody getEncryptedMessage(PushServiceSocket socket, long threadId,
PushAddress pushAddress, byte[] plaintext)
throws IOException, UntrustedIdentityException
{
if (!SessionUtil.hasEncryptCapableSession(context, masterSecret, pushAddress)) {
try {
List preKeys = socket.getPreKeys(pushAddress);
for (PreKeyBundle preKey : preKeys) {
PushAddress device = PushAddress.create(context, pushAddress.getRecipientId(), pushAddress.getNumber(), preKey.getDeviceId());
KeyExchangeProcessor processor = new KeyExchangeProcessor(context, masterSecret, device);
try {
processor.processKeyExchangeMessage(preKey, threadId);
} catch (org.whispersystems.libaxolotl.UntrustedIdentityException e) {
throw new UntrustedIdentityException("Untrusted identity key!", pushAddress.getNumber(), preKey.getIdentityKey());
}
}
} catch (InvalidKeyException e) {
throw new IOException(e);
}
}
TransportDetails transportDetails = new PushTransportDetails(SessionUtil.getSessionVersion(context, masterSecret, pushAddress));
TextSecureCipher cipher = new TextSecureCipher(context, masterSecret, pushAddress, transportDetails);
CiphertextMessage message = cipher.encrypt(plaintext);
int remoteRegistrationId = cipher.getRemoteRegistrationId();
if (message.getType() == CiphertextMessage.PREKEY_TYPE) {
return new PushBody(IncomingPushMessageSignal.Type.PREKEY_BUNDLE_VALUE, remoteRegistrationId, message.serialize());
} else if (message.getType() == CiphertextMessage.WHISPER_TYPE) {
return new PushBody(IncomingPushMessageSignal.Type.CIPHERTEXT_VALUE, remoteRegistrationId, message.serialize());
} else {
throw new AssertionError("Unknown ciphertext type: " + message.getType());
}
}
}