Basic support for encrypted push-based attachments.

1) Move the attachment structures into the encrypted message body.

2) Encrypt attachments with symmetric keys transmitted in the
   encryptd attachment pointer structure.

3) Correctly handle asynchronous decryption and categorization of
   encrypted push messages.

TODO: Correct notification process and network/interruption
      retries.
This commit is contained in:
Moxie Marlinspike
2013-09-08 18:19:05 -07:00
parent cddba2738f
commit 0dd36c64a4
47 changed files with 2381 additions and 1003 deletions

View File

@@ -48,14 +48,8 @@ public class MmsReceiver {
}
public void process(MasterSecret masterSecret, Intent intent) {
try {
if (intent.getAction().equals(SendReceiveService.RECEIVE_MMS_ACTION)) {
handleMmsNotification(intent);
} else if (intent.getAction().equals(SendReceiveService.RECEIVE_PUSH_MMS_ACTION)) {
handlePushMedia(masterSecret, intent);
}
} catch (MmsException e) {
Log.w("MmsReceiver", e);
if (intent.getAction().equals(SendReceiveService.RECEIVE_MMS_ACTION)) {
handleMmsNotification(intent);
}
}
@@ -73,28 +67,6 @@ public class MmsReceiver {
}
}
private void handlePushMedia(MasterSecret masterSecret, Intent intent) throws MmsException {
IncomingPushMessage pushMessage = intent.getParcelableExtra("media_message");
String localNumber = TextSecurePreferences.getLocalNumber(context);
String password = TextSecurePreferences.getPushServerPassword(context);
PushServiceSocket socket = new PushServiceSocket(context, localNumber, password);
try {
List<Pair<File, String>> attachments = socket.retrieveAttachments(pushMessage.getAttachments());
IncomingMediaMessage message = new IncomingMediaMessage(localNumber, pushMessage, attachments);
DatabaseFactory.getMmsDatabase(context).insertMessageInbox(masterSecret, message, "", -1);
} catch (IOException e) {
Log.w("MmsReceiver", e);
try {
IncomingMediaMessage message = new IncomingMediaMessage(localNumber, pushMessage, null);
DatabaseFactory.getMmsDatabase(context).insertMessageInbox(masterSecret, message, "", -1);
} catch (IOException e1) {
throw new MmsException(e1);
}
}
}
private void scheduleDownload(NotificationInd pdu, long messageId, long threadId) {
Intent intent = new Intent(SendReceiveService.DOWNLOAD_MMS_ACTION, null, context, SendReceiveService.class);
intent.putExtra("content_location", new String(pdu.getContentLocation()));

View File

@@ -0,0 +1,107 @@
package org.thoughtcrime.securesms.service;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import android.util.Pair;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.EncryptingPartDatabase;
import org.thoughtcrime.securesms.database.PartDatabase;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.textsecure.crypto.AttachmentCipherInputStream;
import org.whispersystems.textsecure.crypto.InvalidMessageException;
import org.whispersystems.textsecure.crypto.MasterCipher;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.push.PushServiceSocket;
import org.whispersystems.textsecure.util.Base64;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import ws.com.google.android.mms.MmsException;
import ws.com.google.android.mms.pdu.PduPart;
public class PushDownloader {
private final Context context;
public PushDownloader(Context context) {
this.context = context.getApplicationContext();
}
public void process(MasterSecret masterSecret, Intent intent) {
if (!intent.getAction().equals(SendReceiveService.DOWNLOAD_PUSH_ACTION))
return;
long messageId = intent.getLongExtra("message_id", -1);
PartDatabase database = DatabaseFactory.getEncryptingPartDatabase(context, masterSecret);
Log.w("PushDownloader", "Downloading push parts for: " + messageId);
if (messageId != -1) {
List<Pair<Long, PduPart>> parts = database.getParts(messageId, false);
for (Pair<Long, PduPart> partPair : parts) {
retrievePart(masterSecret, partPair.second, messageId, partPair.first);
Log.w("PushDownloader", "Got part: " + partPair.first);
}
} else {
List<Pair<Long, Pair<Long, PduPart>>> parts = database.getPushPendingParts();
for (Pair<Long, Pair<Long, PduPart>> partPair : parts) {
retrievePart(masterSecret, partPair.second.second, partPair.first, partPair.second.first);
Log.w("PushDownloader", "Got part: " + partPair.second.first);
}
}
}
private void retrievePart(MasterSecret masterSecret, PduPart part, long messageId, long partId) {
EncryptingPartDatabase database = DatabaseFactory.getEncryptingPartDatabase(context, masterSecret);
File attachmentFile = null;
try {
MasterCipher masterCipher = new MasterCipher(masterSecret);
long contentLocation = Long.parseLong(Util.toIsoString(part.getContentLocation()));
byte[] key = masterCipher.decryptBytes(Base64.decode(Util.toIsoString(part.getContentDisposition())));
attachmentFile = downloadAttachment(contentLocation);
InputStream attachmentInput = new AttachmentCipherInputStream(attachmentFile, key);
database.updateDownloadedPart(messageId, partId, part, attachmentInput);
} catch (InvalidMessageException e) {
Log.w("PushDownloader", e);
try {
database.updateFailedDownloadedPart(messageId, partId, part);
} catch (MmsException mme) {
Log.w("PushDownloader", mme);
}
} catch (MmsException e) {
Log.w("PushDownloader", e);
try {
database.updateFailedDownloadedPart(messageId, partId, part);
} catch (MmsException mme) {
Log.w("PushDownloader", mme);
}
} catch (IOException e) {
Log.w("PushDownloader", e);
/// XXX schedule some kind of soft failure retry action
} finally {
if (attachmentFile != null)
attachmentFile.delete();
}
}
private File downloadAttachment(long contentLocation) throws IOException {
String localNumber = TextSecurePreferences.getLocalNumber(context);
String password = TextSecurePreferences.getPushServerPassword(context);
PushServiceSocket socket = new PushServiceSocket(context, localNumber, password);
return socket.retrieveAttachment(contentLocation);
}
}

View File

@@ -0,0 +1,211 @@
package org.thoughtcrime.securesms.service;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import android.util.Pair;
import com.google.protobuf.InvalidProtocolBufferException;
import org.thoughtcrime.securesms.crypto.DecryptingQueue;
import org.thoughtcrime.securesms.crypto.KeyExchangeProcessor;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.EncryptingSmsDatabase;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.sms.IncomingEncryptedMessage;
import org.thoughtcrime.securesms.sms.IncomingKeyExchangeMessage;
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.textsecure.crypto.InvalidKeyException;
import org.whispersystems.textsecure.crypto.InvalidVersionException;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.crypto.protocol.PreKeyBundleMessage;
import org.whispersystems.textsecure.push.IncomingPushMessage;
import org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent;
import org.whispersystems.textsecure.storage.InvalidKeyIdException;
import ws.com.google.android.mms.MmsException;
public class PushReceiver {
public static final int RESULT_OK = 0;
public static final int RESULT_NO_SESSION = 1;
public static final int RESULT_DECRYPT_FAILED = 2;
private final Context context;
public PushReceiver(Context context) {
this.context = context.getApplicationContext();
}
public void process(MasterSecret masterSecret, Intent intent) {
if (intent.getAction().equals(SendReceiveService.RECEIVE_PUSH_ACTION)) {
handleMessage(masterSecret, intent);
} else if (intent.getAction().equals(SendReceiveService.DECRYPTED_PUSH_ACTION)) {
handleDecrypt(masterSecret, intent);
}
}
private void handleDecrypt(MasterSecret masterSecret, Intent intent) {
IncomingPushMessage message = intent.getParcelableExtra("message");
long messageId = intent.getLongExtra("message_id", -1);
int result = intent.getIntExtra("result", 0);
if (result == RESULT_OK) handleReceivedMessage(masterSecret, message, true);
else if (result == RESULT_NO_SESSION) handleReceivedMessageForNoSession(masterSecret, message);
else if (result == RESULT_DECRYPT_FAILED) handleReceivedCorruptedMessage(masterSecret, message, true);
DatabaseFactory.getPushDatabase(context).delete(messageId);
}
private void handleMessage(MasterSecret masterSecret, Intent intent) {
IncomingPushMessage message = intent.getExtras().getParcelable("message");
if (message.isSecureMessage()) handleReceivedSecureMessage(masterSecret, message);
else if (message.isPreKeyBundle()) handleReceivedPreKeyBundle(masterSecret, message);
else handleReceivedMessage(masterSecret, message, false);
}
private void handleReceivedSecureMessage(MasterSecret masterSecret, IncomingPushMessage message) {
long id = DatabaseFactory.getPushDatabase(context).insert(message);
DecryptingQueue.scheduleDecryption(context, masterSecret, id, message);
}
private void handleReceivedPreKeyBundle(MasterSecret masterSecret, IncomingPushMessage message) {
try {
Recipient recipient = new Recipient(null, message.getSource(), null, null);
KeyExchangeProcessor processor = new KeyExchangeProcessor(context, masterSecret, recipient);
PreKeyBundleMessage preKeyExchange = new PreKeyBundleMessage(message.getBody());
if (processor.isTrusted(preKeyExchange)) {
processor.processKeyExchangeMessage(preKeyExchange);
IncomingPushMessage bundledMessage = message.withBody(preKeyExchange.getBundledMessage());
handleReceivedSecureMessage(masterSecret, bundledMessage);
} else {
/// XXX
}
} catch (InvalidKeyException e) {
Log.w("SmsReceiver", e);
handleReceivedCorruptedKey(masterSecret, message, false);
} catch (InvalidVersionException e) {
Log.w("SmsReceiver", e);
handleReceivedCorruptedKey(masterSecret, message, true);
} catch (InvalidKeyIdException e) {
Log.w("SmsReceiver", e);
handleReceivedCorruptedKey(masterSecret, message, false);
}
}
private void handleReceivedMessage(MasterSecret masterSecret,
IncomingPushMessage message,
boolean secure)
{
try {
PushMessageContent messageContent = PushMessageContent.parseFrom(message.getBody());
if (messageContent.getAttachmentsCount() > 0) {
Log.w("PushReceiver", "Received push media message...");
handleReceivedMediaMessage(masterSecret, message, messageContent, secure);
} else {
Log.w("PushReceiver", "Received push text message...");
handleReceivedTextMessage(masterSecret, message, messageContent, secure);
}
} catch (InvalidProtocolBufferException e) {
Log.w("PushReceiver", e);
handleReceivedCorruptedMessage(masterSecret, message, secure);
}
}
private void handleReceivedMediaMessage(MasterSecret masterSecret,
IncomingPushMessage message,
PushMessageContent messageContent,
boolean secure)
{
try {
String localNumber = TextSecurePreferences.getLocalNumber(context);
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
IncomingMediaMessage mediaMessage = new IncomingMediaMessage(masterSecret, localNumber,
message, messageContent);
Pair<Long, Long> messageAndThreadId;
if (secure) {
messageAndThreadId = database.insertSecureDecryptedMessageInbox(masterSecret, mediaMessage, -1);
} else {
messageAndThreadId = database.insertMessageInbox(masterSecret, mediaMessage, null, -1);
}
Intent intent = new Intent(context, SendReceiveService.class);
intent.setAction(SendReceiveService.DOWNLOAD_PUSH_ACTION);
intent.putExtra("message_id", messageAndThreadId.first);
context.startService(intent);
} catch (MmsException e) {
Log.w("PushReceiver", e);
// XXX
}
}
private void handleReceivedTextMessage(MasterSecret masterSecret,
IncomingPushMessage message,
PushMessageContent messageContent,
boolean secure)
{
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
IncomingTextMessage textMessage = new IncomingTextMessage(message, "");
if (secure) {
textMessage = new IncomingEncryptedMessage(textMessage, "");
}
Pair<Long, Long> messageAndThreadId = database.insertMessageInbox(masterSecret, textMessage);
database.updateMessageBody(masterSecret, messageAndThreadId.first, messageContent.getBody());
}
private void handleReceivedCorruptedMessage(MasterSecret masterSecret,
IncomingPushMessage message,
boolean secure)
{
long messageId = insertMessagePlaceholder(masterSecret, message, secure);
DatabaseFactory.getEncryptingSmsDatabase(context).markAsDecryptFailed(messageId);
}
private void handleReceivedCorruptedKey(MasterSecret masterSecret,
IncomingPushMessage message,
boolean invalidVersion)
{
IncomingTextMessage corruptedMessage = new IncomingTextMessage(message, "");
IncomingKeyExchangeMessage corruptedKeyMessage = new IncomingKeyExchangeMessage(corruptedMessage, "");
if (!invalidVersion) corruptedKeyMessage.setCorrupted(true);
else corruptedKeyMessage.setInvalidVersion(true);
DatabaseFactory.getEncryptingSmsDatabase(context).insertMessageInbox(masterSecret, corruptedKeyMessage);
}
private void handleReceivedMessageForNoSession(MasterSecret masterSecret,
IncomingPushMessage message)
{
long messageId = insertMessagePlaceholder(masterSecret, message, true);
DatabaseFactory.getEncryptingSmsDatabase(context).markAsNoSession(messageId);
}
private long insertMessagePlaceholder(MasterSecret masterSecret,
IncomingPushMessage message,
boolean secure)
{
IncomingTextMessage placeholder = new IncomingTextMessage(message, "");
if (secure) {
placeholder = new IncomingEncryptedMessage(placeholder, "");
}
Pair<Long, Long> messageAndThreadId = DatabaseFactory.getEncryptingSmsDatabase(context)
.insertMessageInbox(masterSecret,
placeholder);
return messageAndThreadId.first;
}
}

View File

@@ -51,10 +51,12 @@ public class SendReceiveService extends Service {
public static final String RECEIVE_SMS_ACTION = "org.thoughtcrime.securesms.SendReceiveService.RECEIVE_SMS_ACTION";
public static final String SEND_MMS_ACTION = "org.thoughtcrime.securesms.SendReceiveService.SEND_MMS_ACTION";
public static final String RECEIVE_MMS_ACTION = "org.thoughtcrime.securesms.SendReceiveService.RECEIVE_MMS_ACTION";
public static final String RECEIVE_PUSH_MMS_ACTION = "org.thoughtcrime.securesms.SendReceiveService.RECEIVE_PUSH_MMS_ACTION";
public static final String DOWNLOAD_MMS_ACTION = "org.thoughtcrime.securesms.SendReceiveService.DOWNLOAD_MMS_ACTION";
public static final String DOWNLOAD_MMS_CONNECTIVITY_ACTION = "org.thoughtcrime.securesms.SendReceiveService.DOWNLOAD_MMS_CONNECTIVITY_ACTION";
public static final String DOWNLOAD_MMS_PENDING_APN_ACTION = "org.thoughtcrime.securesms.SendReceiveService.DOWNLOAD_MMS_PENDING_APN_ACTION";
public static final String RECEIVE_PUSH_ACTION = "org.thoughtcrime.securesms.SendReceiveService.RECEIVE_PUSH_ACTION";
public static final String DECRYPTED_PUSH_ACTION = "org.thoughtcrime.securesms.SendReceiveService.DECRYPTED_PUSH_ACTION";
public static final String DOWNLOAD_PUSH_ACTION = "org.thoughtcrime.securesms.SendReceiveService.DOWNLOAD_PUSH_ACTION";
private static final int SEND_SMS = 0;
private static final int RECEIVE_SMS = 1;
@@ -62,14 +64,18 @@ public class SendReceiveService extends Service {
private static final int RECEIVE_MMS = 3;
private static final int DOWNLOAD_MMS = 4;
private static final int DOWNLOAD_MMS_PENDING = 5;
private static final int RECEIVE_PUSH = 6;
private static final int DOWNLOAD_PUSH = 7;
private ToastHandler toastHandler;
private SmsReceiver smsReceiver;
private SmsSender smsSender;
private MmsReceiver mmsReceiver;
private MmsSender mmsSender;
private MmsDownloader mmsDownloader;
private SmsReceiver smsReceiver;
private SmsSender smsSender;
private MmsReceiver mmsReceiver;
private MmsSender mmsSender;
private MmsDownloader mmsDownloader;
private PushReceiver pushReceiver;
private PushDownloader pushDownloader;
private MasterSecret masterSecret;
private boolean hasSecret;
@@ -78,7 +84,6 @@ public class SendReceiveService extends Service {
private ClearKeyReceiver clearKeyReceiver;
private List<Runnable> workQueue;
private List<Runnable> pendingSecretList;
private Thread workerThread;
@Override
public void onCreate() {
@@ -105,12 +110,18 @@ public class SendReceiveService extends Service {
scheduleIntent(SEND_SMS, intent);
else if (action.equals(SEND_MMS_ACTION))
scheduleSecretRequiredIntent(SEND_MMS, intent);
else if (action.equals(RECEIVE_MMS_ACTION) || action.equals(RECEIVE_PUSH_MMS_ACTION))
else if (action.equals(RECEIVE_MMS_ACTION))
scheduleIntent(RECEIVE_MMS, intent);
else if (action.equals(DOWNLOAD_MMS_ACTION))
scheduleSecretRequiredIntent(DOWNLOAD_MMS, intent);
else if (intent.getAction().equals(DOWNLOAD_MMS_PENDING_APN_ACTION))
scheduleSecretRequiredIntent(DOWNLOAD_MMS_PENDING, intent);
else if (action.equals(RECEIVE_PUSH_ACTION))
scheduleIntent(RECEIVE_PUSH, intent);
else if (action.equals(DECRYPTED_PUSH_ACTION))
scheduleSecretRequiredIntent(RECEIVE_PUSH, intent);
else if (action.equals(DOWNLOAD_PUSH_ACTION))
scheduleSecretRequiredIntent(DOWNLOAD_PUSH, intent);
else
Log.w("SendReceiveService", "Received intent with unknown action: " + intent.getAction());
}
@@ -142,13 +153,15 @@ public class SendReceiveService extends Service {
mmsReceiver = new MmsReceiver(this);
mmsSender = new MmsSender(this, toastHandler);
mmsDownloader = new MmsDownloader(this, toastHandler);
pushReceiver = new PushReceiver(this);
pushDownloader = new PushDownloader(this);
}
private void initializeWorkQueue() {
pendingSecretList = new LinkedList<Runnable>();
workQueue = new LinkedList<Runnable>();
workerThread = new WorkerThread(workQueue, "SendReceveService-WorkerThread");
Thread workerThread = new WorkerThread(workQueue, "SendReceveService-WorkerThread");
workerThread.start();
}
@@ -222,12 +235,14 @@ public class SendReceiveService extends Service {
@Override
public void run() {
switch (what) {
case RECEIVE_SMS: smsReceiver.process(masterSecret, intent); return;
case SEND_SMS: smsSender.process(masterSecret, intent); return;
case RECEIVE_MMS: mmsReceiver.process(masterSecret, intent); return;
case SEND_MMS: mmsSender.process(masterSecret, intent); return;
case DOWNLOAD_MMS: mmsDownloader.process(masterSecret, intent); return;
case RECEIVE_SMS: smsReceiver.process(masterSecret, intent); return;
case SEND_SMS: smsSender.process(masterSecret, intent); return;
case RECEIVE_MMS: mmsReceiver.process(masterSecret, intent); return;
case SEND_MMS: mmsSender.process(masterSecret, intent); return;
case DOWNLOAD_MMS: mmsDownloader.process(masterSecret, intent); return;
case DOWNLOAD_MMS_PENDING: mmsDownloader.process(masterSecret, intent); return;
case RECEIVE_PUSH: pushReceiver.process(masterSecret, intent); return;
case DOWNLOAD_PUSH: pushDownloader.process(masterSecret, intent); return;
}
}
}