Initial Project Import

This commit is contained in:
Moxie Marlinspike
2011-12-20 10:20:44 -08:00
commit bbea3fe1b1
397 changed files with 48065 additions and 0 deletions

View File

@@ -0,0 +1,238 @@
/**
* Copyright (C) 2011 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.service;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.SecureSMS;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import android.app.AlarmManager;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Binder;
import android.os.IBinder;
import android.os.SystemClock;
import android.preference.PreferenceManager;
import android.util.Log;
/**
* Small service that stays running to keep a key cached in memory.
*
* @author Moxie Marlinspike
*/
public class KeyCachingService extends Service {
public static final int NOTIFICATION_ID = 1337;
public static final int SERVICE_RUNNING_ID = 4141;
public static final String KEY_PERMISSION = "org.thoughtcrime.securesms.ACCESS_SECRETS";
public static final String NEW_KEY_EVENT = "org.thoughtcrime.securesms.service.action.NEW_KEY_EVENT";
public static final String PASSPHRASE_EXPIRED_EVENT = "org.thoughtcrime.securesms.service.action.PASSPHRASE_EXPIRED_EVENT";
public static final String CLEAR_KEY_ACTION = "org.thoughtcrime.securesms.service.action.CLEAR_KEY";
public static final String ACTIVITY_START_EVENT = "org.thoughtcrime.securesms.service.action.ACTIVITY_START_EVENT";
public static final String ACTIVITY_STOP_EVENT = "org.thoughtcrime.securesms.service.action.ACTIVITY_STOP_EVENT";
public static final String PREFERENCES_NAME = "SecureSMS-Preferences";
private static final Class[] mStartForegroundSignature = new Class[] {int.class, Notification.class};
private static final Class[] mStopForegroundSignature = new Class[] {boolean.class};
private PendingIntent pending;
private NotificationManager notificationManager;
private Method mStartForeground;
private Method mStopForeground;
private Object[] mStartForegroundArgs = new Object[2];
private Object[] mStopForegroundArgs = new Object[1];
private int activitiesRunning = 0;
private final IBinder binder = new KeyCachingBinder();
private MasterSecret masterSecret;
public KeyCachingService() {}
public synchronized MasterSecret getMasterSecret() {
return masterSecret;
}
public synchronized void setMasterSecret(MasterSecret masterSecret) {
this.masterSecret = masterSecret;
foregroundService();
broadcastNewSecret();
startTimeoutIfAppropriate();
}
@Override
public void onStart(Intent intent, int startId) {
if (intent.getAction() != null && intent.getAction().equals(CLEAR_KEY_ACTION))
handleClearKey();
else if (intent.getAction() != null && intent.getAction().equals(ACTIVITY_START_EVENT))
handleActivityStarted();
else if (intent.getAction() != null && intent.getAction().equals(ACTIVITY_STOP_EVENT))
handleActivityStopped();
else if (intent.getAction() != null && intent.getAction().equals(PASSPHRASE_EXPIRED_EVENT))
handlePassphraseExpired();
}
@Override
public void onCreate() {
pending = PendingIntent.getService(this, 0, new Intent(PASSPHRASE_EXPIRED_EVENT, null, this, KeyCachingService.class), 0);
notificationManager = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
try {
mStartForeground = getClass().getMethod("startForeground", mStartForegroundSignature);
mStopForeground = getClass().getMethod("stopForeground", mStopForegroundSignature);
} catch (NoSuchMethodException e) {
// Running on an older platform.
mStartForeground = mStopForeground = null;
}
}
@Override
public void onDestroy() {
Log.e("kcs", "KCS Is Being Destroyed!");
}
private void handleActivityStarted() {
Log.w("KeyCachingService", "Incrementing activity count...");
AlarmManager alarmManager = (AlarmManager)this.getSystemService(ALARM_SERVICE);
alarmManager.cancel(pending);
activitiesRunning++;
}
private void handleActivityStopped() {
Log.w("KeyCachingService", "Decrementing activity count...");
activitiesRunning--;
startTimeoutIfAppropriate();
}
private void handleClearKey() {
this.masterSecret = null;
stopForegroundCompat(SERVICE_RUNNING_ID);
}
private void handlePassphraseExpired() {
handleClearKey();
Intent intent = new Intent(PASSPHRASE_EXPIRED_EVENT);
intent.setPackage(getApplicationContext().getPackageName());
sendBroadcast(intent, KEY_PERMISSION);
}
private void startTimeoutIfAppropriate() {
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
boolean timeoutEnabled = sharedPreferences.getBoolean(ApplicationPreferencesActivity.PASSPHRASE_TIMEOUT_PREF, false);
if ((activitiesRunning == 0) && (this.masterSecret != null) && timeoutEnabled) {
long timeoutMinutes = sharedPreferences.getInt(ApplicationPreferencesActivity.PASSPHRASE_TIMEOUT_INTERVAL_PREF, 60 * 5);
long timeoutMillis = timeoutMinutes * 60 * 1000;
Log.w("KeyCachingService", "Starting timeout: " + timeoutMillis);
AlarmManager alarmManager = (AlarmManager)this.getSystemService(ALARM_SERVICE);
alarmManager.cancel(pending);
alarmManager.set(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + timeoutMillis, pending);
}
}
private void foregroundService() {
Notification notification = new Notification(R.drawable.icon, "TextSecure Passphrase Cached", System.currentTimeMillis());
Intent intent = new Intent(this, SecureSMS.class);
PendingIntent launchIntent = PendingIntent.getActivity(getApplicationContext(), 0, intent, 0);
notification.setLatestEventInfo(getApplicationContext(), "TextSecure Cached", "TextSecure Passphrase Cached", launchIntent);
stopForegroundCompat(SERVICE_RUNNING_ID);
startForegroundCompat(SERVICE_RUNNING_ID, notification);
}
private void broadcastNewSecret() {
Log.w("service", "Broadcasting new secret...");
Intent intent = new Intent(NEW_KEY_EVENT);
intent.putExtra("master_secret", masterSecret);
intent.setPackage(getApplicationContext().getPackageName());
sendBroadcast(intent, KEY_PERMISSION);
}
@Override
public IBinder onBind(Intent arg0) {
return binder;
}
public class KeyCachingBinder extends Binder {
public KeyCachingService getService() {
return KeyCachingService.this;
}
}
/**
* This is a wrapper around the new startForeground method, using the older
* APIs if it is not available.
*/
private void startForegroundCompat(int id, Notification notification) {
if (mStartForeground != null) {
mStartForegroundArgs[0] = Integer.valueOf(id);
mStartForegroundArgs[1] = notification;
try {
mStartForeground.invoke(this, mStartForegroundArgs);
} catch (InvocationTargetException e) {
Log.w("KeyCachingService", "Unable to invoke startForeground", e);
} catch (IllegalAccessException e) {
Log.w("KeyCachingService", "Unable to invoke startForeground", e);
}
return;
}
setForeground(true);
notificationManager.notify(id, notification);
}
/**
* This is a wrapper around the new stopForeground method, using the older
* APIs if it is not available.
*/
private void stopForegroundCompat(int id) {
Log.w("KeyCachingService", "Calling stopForeground!");
if (mStopForeground != null) {
mStopForegroundArgs[0] = Boolean.TRUE;
try {
mStopForeground.invoke(this, mStopForegroundArgs);
} catch (InvocationTargetException e) {
Log.w("KeyCachingService", "Unable to invoke stopForeground", e);
} catch (IllegalAccessException e) {
Log.w("KeyCachingService", "Unable to invoke stopForeground", e);
}
return;
}
notificationManager.cancel(id);
setForeground(false);
}
}

View File

@@ -0,0 +1,201 @@
/**
* Copyright (C) 2011 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.service;
import java.util.LinkedList;
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.SecureSMS;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase;
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 android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.graphics.Color;
import android.net.Uri;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import android.util.Log;
/**
* Handles posting system notifications for new messages.
*
*
* @author Moxie Marlinspike
*/
public class MessageNotifier {
public static final int NOTIFICATION_ID = 1338;
private static String buildTickerMessage(int count, Recipients recipients) {
Recipient recipient = recipients.getPrimaryRecipient();
StringBuilder builder = new StringBuilder();
builder.append('(');
builder.append(count);
builder.append(')');
builder.append(" New messages");
if (recipient != null) {
builder.append(", most recent from: ");
builder.append(recipient.getName() == null ? recipient.getNumber() : recipient.getName());
}
return builder.toString();
}
private static String buildTitleMessage(int count) {
return "(" + count + ") New Messages";
}
private static String buildSubtitleMessage(Recipients recipients) {
Recipient recipient = recipients.getPrimaryRecipient();
if (recipient != null) {
return "Most recent from: " + (recipient.getName() == null ? recipient.getNumber() : recipient.getName());
}
return null;
}
private static Recipients getSmsRecipient(Context context, Cursor c) throws RecipientFormattingException {
String address = c.getString(c.getColumnIndexOrThrow(SmsDatabase.ADDRESS));
return RecipientFactory.getRecipientsFromString(context, address);
}
private static Recipients getMmsRecipient(Context context, Cursor c) throws RecipientFormattingException {
long messageId = c.getLong(c.getColumnIndexOrThrow(MmsDatabase.ID));
String address = DatabaseFactory.getMmsDatabase(context).getMessageRecipient(messageId);
return RecipientFactory.getRecipientsFromString(context, address);
}
private static Recipients getMostRecentRecipients(Context context, Cursor c) {
if (c != null && c.moveToLast()) {
try {
String type = c.getString(c.getColumnIndexOrThrow(MmsSmsDatabase.TRANSPORT));
if (type.equals("sms"))
return getSmsRecipient(context, c);
else
return getMmsRecipient(context, c);
} catch (RecipientFormattingException e) {
return new Recipients(new LinkedList<Recipient>());
}
}
return null;
}
private static PendingIntent buildPendingIntent(Context context, Cursor c, Recipients recipients) {
Intent intent = new Intent(context, SecureSMS.class);
intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
Log.w("SMSNotifier", "Building pending intent...");
if (c != null && c.getCount() == 1) {
Log.w("SMSNotifier", "Adding extras...");
c.moveToLast();
long threadId = c.getLong(c.getColumnIndexOrThrow(SmsDatabase.THREAD_ID));
Log.w("SmsNotifier", "Adding thread_id to pending intent: " + threadId);
if (recipients.getPrimaryRecipient() != null) {
intent.putExtra("recipients", recipients);
intent.putExtra("thread_id", threadId);
}
intent.setData((Uri.parse("custom://"+System.currentTimeMillis())));
}
return PendingIntent.getActivity(context, 0, intent, 0);
}
private static void sendNotification(Context context, NotificationManager manager, PendingIntent launchIntent,
String ticker, String title, String subtitle, boolean signal)
{
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
if (!sp.getBoolean(ApplicationPreferencesActivity.NOTIFICATION_PREF, true)) return;
Notification notification = new Notification(R.drawable.stat_notify_sms, ticker, System.currentTimeMillis());
String ringtone = sp.getString(ApplicationPreferencesActivity.RINGTONE_PREF, null);
boolean vibrate = sp.getBoolean(ApplicationPreferencesActivity.VIBRATE_PREF, true);
String ledColor = sp.getString(ApplicationPreferencesActivity.LED_COLOR_PREF, "green");
String ledBlinkPattern = sp.getString(ApplicationPreferencesActivity.LED_BLINK_PREF, "500,2000");
String ledBlinkPatternCustom = sp.getString(ApplicationPreferencesActivity.LED_BLINK_PREF_CUSTOM, "500,2000");
String[] blinkPatternArray = parseBlinkPattern(ledBlinkPattern, ledBlinkPatternCustom);
notification.setLatestEventInfo(context, title, subtitle, launchIntent);
notification.sound = TextUtils.isEmpty(ringtone) || !signal ? null : Uri.parse(ringtone);
if (signal && vibrate)
notification.defaults |= Notification.DEFAULT_VIBRATE;
notification.flags |= Notification.FLAG_SHOW_LIGHTS;
notification.ledARGB = Color.parseColor(ledColor);//0xff00ff00;
notification.ledOnMS = Integer.parseInt(blinkPatternArray[0]);
notification.ledOffMS = Integer.parseInt(blinkPatternArray[1]);
manager.notify(NOTIFICATION_ID, notification);
}
private static void flashNotification(Context context, NotificationManager manager) {
sendNotification(context, manager, buildPendingIntent(context, null, null), "(1) New Messages", "(1) New Messages", null, true);
}
public static void updateNotification(Context context, boolean signal) {
NotificationManager manager = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
manager.cancel(NOTIFICATION_ID);
Cursor c = null;
try {
c = DatabaseFactory.getMmsSmsDatabase(context).getUnread();
if ((c == null && signal) || (!c.moveToFirst() && signal)) {flashNotification(context, manager); return;}
else if (c == null || !c.moveToFirst()) return;
Recipients recipients = getMostRecentRecipients(context, c);
String ticker = buildTickerMessage(c.getCount(), recipients);
String title = buildTitleMessage(c.getCount());
String subtitle = buildSubtitleMessage(recipients);
PendingIntent launchIntent = buildPendingIntent(context, c, recipients);
sendNotification(context, manager, launchIntent, ticker, title, subtitle, signal);
} finally {
if (c != null)
c.close();
}
}
private static String[] parseBlinkPattern(String blinkPattern, String blinkPatternCustom) {
if (blinkPattern.equals("custom"))
blinkPattern = blinkPatternCustom;
return blinkPattern.split(",");
}
}

View File

@@ -0,0 +1,180 @@
/**
* Copyright (C) 2011 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.service;
import java.io.IOException;
import java.util.LinkedList;
import org.thoughtcrime.securesms.crypto.DecryptingQueue;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.mms.MmsDownloadHelper;
import org.thoughtcrime.securesms.protocol.WirePrefix;
import ws.com.google.android.mms.MmsException;
import ws.com.google.android.mms.pdu.PduParser;
import ws.com.google.android.mms.pdu.RetrieveConf;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
public class MmsDownloader extends MmscProcessor {
private final LinkedList<DownloadItem> pendingMessages = new LinkedList<DownloadItem>();
private final SendReceiveService.ToastHandler toastHandler;
public MmsDownloader(Context context, SendReceiveService.ToastHandler toastHandler) {
super(context);
this.toastHandler = toastHandler;
}
private void handleDownloadMms(DownloadItem item) {
if (!isConnectivityPossible()) {
DatabaseFactory.getMmsDatabase(context).markDownloadState(item.getMessageId(), MmsDatabase.Types.DOWNLOAD_NO_CONNECTIVITY);
toastHandler.makeToast("No connectivity available for MMS download, try again later...");
Log.w("MmsDownloadService", "Unable to download MMS, please try again later.");
} else {
DatabaseFactory.getMmsDatabase(context).markDownloadState(item.getMessageId(), MmsDatabase.Types.DOWNLOAD_CONNECTING);
pendingMessages.add(item);
issueConnectivityRequest();
}
}
private void handleDownloadMmsContinued(DownloadItem item) {
Log.w("MmsDownloadService", "Handling MMS download continuation...");
MmsDatabase mmsDatabase;
if (item.getMasterSecret() == null)
mmsDatabase = DatabaseFactory.getMmsDatabase(context);
else
mmsDatabase = DatabaseFactory.getEncryptingMmsDatabase(context, item.getMasterSecret());
try {
byte[] pdu = MmsDownloadHelper.retrieveMms(context, item.getContentLocation());
RetrieveConf retrieved = (RetrieveConf)new PduParser(pdu).parse();
for (int i=0;i<retrieved.getBody().getPartsNum();i++) {
Log.w("MmsDownloader", "Sent MMS part of content-type: " + new String(retrieved.getBody().getPart(i).getContentType()));
}
if (retrieved == null)
throw new IOException("Bad retrieved PDU");
if (retrieved.getSubject() != null && WirePrefix.isEncryptedMmsSubject(retrieved.getSubject().getString())) {
long messageId = mmsDatabase.insertSecureMessageReceived(retrieved, item.getContentLocation(), item.getThreadId());
if (item.getMasterSecret() != null)
DecryptingQueue.scheduleDecryption(context, item.getMasterSecret(), messageId, item.getThreadId(), retrieved);
} else {
mmsDatabase.insertMessageReceived(retrieved, item.getContentLocation(), item.getThreadId());
}
mmsDatabase.delete(item.getMessageId());
// NotifyRespInd notifyResponse = new NotifyRespInd(PduHeaders.CURRENT_MMS_VERSION, item.getTransactionId(), PduHeaders.STATUS_RETRIEVED);
// MmsSendHelper.sendMms(context, new PduComposer(context, notifyResponse).make());
if (this.pendingMessages.isEmpty())
finishConnectivity();
} catch (IOException e) {
DatabaseFactory.getMmsDatabase(context).markDownloadState(item.getMessageId(), MmsDatabase.Types.DOWNLOAD_SOFT_FAILURE);
toastHandler.makeToast("Error connecting to MMS provider...");
Log.w("MmsDownloader", e);
} catch (MmsException e) {
DatabaseFactory.getMmsDatabase(context).markDownloadState(item.getMessageId(), MmsDatabase.Types.DOWNLOAD_HARD_FAILURE);
toastHandler.makeToast("Error downloading MMS!");
Log.w("MmsDownloader", e);
}
}
protected void handleConnectivityChange() {
if (!isConnected()) {
if (!isConnectivityPossible() && !pendingMessages.isEmpty()) {
DatabaseFactory.getMmsDatabase(context).markDownloadState(pendingMessages.remove().getMessageId(), MmsDatabase.Types.DOWNLOAD_NO_CONNECTIVITY);
toastHandler.makeToast("No connectivity available for MMS download, try again later...");
Log.w("MmsDownloadService", "Unable to download MMS, please try again later.");
finishConnectivity();
}
return;
}
if (!pendingMessages.isEmpty()) handleDownloadMmsContinued(pendingMessages.remove());
else finishConnectivity();
}
public void process(MasterSecret masterSecret, Intent intent) {
if (intent.getAction().equals(SendReceiveService.DOWNLOAD_MMS_ACTION)) {
DownloadItem item = new DownloadItem(intent.getLongExtra("message_id", -1),
intent.getLongExtra("thread_id", -1),
intent.getStringExtra("content_location"),
intent.getByteArrayExtra("transaction_id"),
masterSecret);
handleDownloadMms(item);
} else if (intent.getAction().equals(SendReceiveService.DOWNLOAD_MMS_CONNECTIVITY_ACTION)) {
handleConnectivityChange();
}
}
private class DownloadItem {
private long threadId;
private long messageId;
private byte[] transactionId;
private String contentLocation;
private MasterSecret masterSecret;
public DownloadItem(long messageId, long threadId, String contentLocation, byte[] transactionId, MasterSecret masterSecret) {
this.threadId = threadId;
this.messageId = messageId;
this.contentLocation = contentLocation;
this.masterSecret = masterSecret;
this.transactionId = transactionId;
}
public long getThreadId() {
return threadId;
}
public long getMessageId() {
return messageId;
}
public String getContentLocation() {
return contentLocation;
}
public byte[] getTransactionId() {
return transactionId;
}
public MasterSecret getMasterSecret() {
return masterSecret;
}
}
@Override
protected String getConnectivityAction() {
return SendReceiveService.DOWNLOAD_MMS_CONNECTIVITY_ACTION;
}
}

View File

@@ -0,0 +1,73 @@
/**
* Copyright (C) 2011 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.service;
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
import org.thoughtcrime.securesms.protocol.WirePrefix;
import ws.com.google.android.mms.pdu.GenericPdu;
import ws.com.google.android.mms.pdu.NotificationInd;
import ws.com.google.android.mms.pdu.PduHeaders;
import ws.com.google.android.mms.pdu.PduParser;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.preference.PreferenceManager;
import android.util.Log;
public class MmsListener extends BroadcastReceiver {
private boolean isRelevent(Context context, Intent intent) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ECLAIR)
return false;
if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean(ApplicationPreferencesActivity.ALL_MMS_PERF, true))
return true;
byte[] mmsData = intent.getByteArrayExtra("data");
PduParser parser = new PduParser(mmsData);
GenericPdu pdu = parser.parse();
if (pdu.getMessageType() != PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND)
return false;
NotificationInd notificationPdu = (NotificationInd)pdu;
if (notificationPdu.getSubject() == null)
return false;
return WirePrefix.isEncryptedMmsSubject(notificationPdu.getSubject().getString());
}
@Override
public void onReceive(Context context, Intent intent) {
Log.w("MmsListener", "Got MMS broadcast...");
if (isRelevent(context, intent)) {
intent.setAction(SendReceiveService.RECEIVE_MMS_ACTION);
intent.putExtra("ResultCode", this.getResultCode());
intent.setClass(context, SendReceiveService.class);
context.startService(intent);
abortBroadcast();
}
}
}

View File

@@ -0,0 +1,69 @@
/**
* Copyright (C) 2011 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.service;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsDatabase;
import ws.com.google.android.mms.pdu.GenericPdu;
import ws.com.google.android.mms.pdu.NotificationInd;
import ws.com.google.android.mms.pdu.PduHeaders;
import ws.com.google.android.mms.pdu.PduParser;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
public class MmsReceiver {
private final Context context;
public MmsReceiver(Context context) {
this.context = context;
}
private void scheduleDownload(NotificationInd pdu, long messageId) {
Intent intent = new Intent(SendReceiveService.DOWNLOAD_MMS_ACTION, null, context, SendReceiveService.class);
intent.putExtra("content_location", new String(pdu.getContentLocation()));
intent.putExtra("message_id", messageId);
intent.putExtra("transaction_id", pdu.getTransactionId());
intent.putExtra("thread_id", DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(messageId));
context.startService(intent);
}
public void process(MasterSecret masterSecret, Intent intent) {
byte[] mmsData = intent.getByteArrayExtra("data");
PduParser parser = new PduParser(mmsData);
GenericPdu pdu = parser.parse();
if (pdu.getMessageType() == PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND) {
MmsDatabase database;
if (masterSecret != null)
database = DatabaseFactory.getEncryptingMmsDatabase(context, masterSecret);
else
database = DatabaseFactory.getMmsDatabase(context);
long messageId = database.insertMessageReceived((NotificationInd)pdu);
MessageNotifier.updateNotification(context, true);
scheduleDownload((NotificationInd)pdu, messageId);
Log.w("MmsReceiverService", "Inserted received notification...");
}
}
}

View File

@@ -0,0 +1,202 @@
/**
* Copyright (C) 2011 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.service;
import java.io.IOException;
import java.util.Arrays;
import java.util.LinkedList;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.SessionCipher;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.mms.MmsSendHelper;
import org.thoughtcrime.securesms.mms.PngTransport;
import org.thoughtcrime.securesms.mms.TextTransport;
import org.thoughtcrime.securesms.protocol.WirePrefix;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.Hex;
import ws.com.google.android.mms.ContentType;
import ws.com.google.android.mms.MmsException;
import ws.com.google.android.mms.pdu.CharacterSets;
import ws.com.google.android.mms.pdu.EncodedStringValue;
import ws.com.google.android.mms.pdu.PduBody;
import ws.com.google.android.mms.pdu.PduComposer;
import ws.com.google.android.mms.pdu.PduHeaders;
import ws.com.google.android.mms.pdu.PduParser;
import ws.com.google.android.mms.pdu.PduPart;
import ws.com.google.android.mms.pdu.SendConf;
import ws.com.google.android.mms.pdu.SendReq;
import android.content.Context;
import android.content.Intent;
import android.telephony.TelephonyManager;
import android.util.Log;
public class MmsSender extends MmscProcessor {
private final LinkedList<SendReq[]> pendingMessages = new LinkedList<SendReq[]>();
public MmsSender(Context context) {
super(context);
}
public void process(MasterSecret masterSecret, Intent intent) {
if (intent.getAction().equals(SendReceiveService.SEND_MMS_ACTION)) {
long messageId = intent.getLongExtra("message_id", -1);
MmsDatabase database = DatabaseFactory.getEncryptingMmsDatabase(context, masterSecret);
try {
SendReq[] sendRequests;
if (messageId == -1) {
sendRequests = database.getOutgoingMessages();
} else {
sendRequests = new SendReq[1];
sendRequests[0] = database.getSendRequest(messageId);
}
if (sendRequests.length > 0)
handleSendMms(sendRequests);
} catch (MmsException me) {
Log.w("MmsSender", me);
if (messageId != -1)
database.markAsSentFailed(messageId);
}
} else if (intent.getAction().equals(SendReceiveService.SEND_MMS_CONNECTIVITY_ACTION)) {
handleConnectivityChange(masterSecret);
}
}
protected void handleConnectivityChange(MasterSecret masterSecret) {
if (!isConnected())
return;
if (!pendingMessages.isEmpty()) handleSendMmsContinued(masterSecret, pendingMessages.remove());
else finishConnectivity();
}
private void handleSendMms(SendReq[] sendRequests) {
if (!isConnectivityPossible()) {
for (int i=0;i<sendRequests.length;i++)
DatabaseFactory.getMmsDatabase(context).markAsSentFailed(sendRequests[i].getDatabaseMessageId());
} else {
pendingMessages.add(sendRequests);
issueConnectivityRequest();
}
}
private boolean isInconsistentResponse(SendReq send, SendConf response) {
Log.w("MmsSenderService", "Comparing: " + Hex.toString(send.getTransactionId()));
Log.w("MmsSenderService", "With: " + Hex.toString(response.getTransactionId()));
return !Arrays.equals(send.getTransactionId(), response.getTransactionId());
}
private byte[] getEncryptedPdu(MasterSecret masterSecret, String recipient, byte[] pduBytes) {
synchronized (SessionCipher.CIPHER_LOCK) {
SessionCipher cipher = new SessionCipher(context, masterSecret, new Recipient(null, recipient, null), new TextTransport());
return cipher.encryptMessage(pduBytes);
}
}
private SendReq getEncryptedMms(MasterSecret masterSecret, SendReq pdu, long messageId) {
Log.w("MmsSender", "Sending Secure MMS.");
EncodedStringValue[] encodedRecipient = pdu.getTo();
String recipient = encodedRecipient[0].getString();
byte[] pduBytes = new PduComposer(context, pdu).make();
byte[] encryptedPdu = getEncryptedPdu(masterSecret, recipient, pduBytes);
Log.w("MmsSendeR", "Got encrypted bytes: " + encryptedPdu.length);
PduBody body = new PduBody();
PduPart part = new PduPart();
part.setContentId((System.currentTimeMillis()+"").getBytes());
part.setContentType(ContentType.TEXT_PLAIN.getBytes());
part.setName((System.currentTimeMillis()+"").getBytes());
part.setData(encryptedPdu);
body.addPart(part);
pdu.setSubject(new EncodedStringValue(WirePrefix.calculateEncryptedMmsSubject()));
pdu.setBody(body);
return pdu;
}
private void sendMms(MmsDatabase db, SendReq pdu, String number, long messageId, boolean secure) {
try {
if (number != null && number.trim().length() != 0)
pdu.setFrom(new EncodedStringValue(number));
byte[] response = MmsSendHelper.sendMms(context, new PduComposer(context, pdu).make());
SendConf conf = (SendConf) new PduParser(response).parse();
for (int i=0;i<pdu.getBody().getPartsNum();i++) {
Log.w("MmsSender", "Sent MMS part of content-type: " + new String(pdu.getBody().getPart(i).getContentType()));
}
if (conf == null) {
db.markAsSentFailed(messageId);
Log.w("MmsSender", "No M-Send.conf received in response to send.");
return;
} else if (conf.getResponseStatus() != PduHeaders.RESPONSE_STATUS_OK) {
Log.w("MmsSender", "Got bad response: " + conf.getResponseStatus());
db.updateResponseStatus(messageId, conf.getResponseStatus());
db.markAsSentFailed(messageId);
return;
} else if (isInconsistentResponse(pdu, conf)) {
db.markAsSentFailed(messageId);
Log.w("MmsSender", "Got a response for the wrong transaction?");
return;
} else {
Log.w("MmsSender", "Successful send! " + messageId);
if (!secure)
db.markAsSent(messageId, conf.getMessageId(), conf.getResponseStatus());
else
db.markAsSecureSent(messageId, conf.getMessageId(), conf.getResponseStatus());
}
} catch (IOException ioe) {
Log.w("MmsSender", ioe);
db.markAsSentFailed(messageId);
}
}
private void handleSendMmsContinued(MasterSecret masterSecret, SendReq[] requests) {
Log.w("MmsSenderService", "Handling MMS send continuation...");
MmsDatabase db = DatabaseFactory.getEncryptingMmsDatabase(context, masterSecret);
String number = ((TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE)).getLine1Number();
for (int i=0;i<requests.length;i++) {
SendReq request = requests[i];
long messageId = request.getDatabaseMessageId();
long messageBox = request.getDatabaseMessageBox();
if (MmsDatabase.Types.isSecureMmsBox(messageBox))
request = getEncryptedMms(masterSecret, request, messageId);
sendMms(db, request, number, messageId, MmsDatabase.Types.isSecureMmsBox(messageBox));
}
if (this.pendingMessages.isEmpty())
finishConnectivity();
}
@Override
protected String getConnectivityAction() {
return SendReceiveService.SEND_MMS_CONNECTIVITY_ACTION;
}
}

View File

@@ -0,0 +1,111 @@
/**
* Copyright (C) 2011 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.service;
import java.util.LinkedList;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
import android.util.Log;
public abstract class MmscProcessor {
private static final String FEATURE_ENABLE_MMS = "enableMMS";
private static final int APN_ALREADY_ACTIVE = 0;
public static final int TYPE_MOBILE_MMS = 2;
private ConnectivityManager connectivityManager;
private ConnectivityListener connectivityListener;
private WakeLock wakeLock;
protected final Context context;
public MmscProcessor(Context context) {
this.context = context;
PowerManager powerManager = (PowerManager)context.getSystemService(Context.POWER_SERVICE);
this.connectivityManager = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
this.wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MMS Connection");
this.wakeLock.setReferenceCounted(false);
}
protected boolean isConnected() {
NetworkInfo info = connectivityManager.getNetworkInfo(TYPE_MOBILE_MMS);
Log.w("MmsService", "NetworkInfo: " + info);
if ((info == null) || (info.getType() != TYPE_MOBILE_MMS) || !info.isConnected())
return false;
return true;
}
protected abstract String getConnectivityAction();
protected void issueConnectivityRequest() {
int status = connectivityManager.startUsingNetworkFeature(ConnectivityManager.TYPE_MOBILE, FEATURE_ENABLE_MMS);
if (status == APN_ALREADY_ACTIVE) {
issueConnectivityChange();
} else if (connectivityListener == null) {
IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
connectivityListener = new ConnectivityListener();
context.registerReceiver(connectivityListener, filter);
wakeLock.acquire();
}
}
protected boolean isConnectivityPossible() {
NetworkInfo networkInfo = connectivityManager.getNetworkInfo(TYPE_MOBILE_MMS);
Log.w("MmsService", "Got network info: " + networkInfo);
return networkInfo != null && networkInfo.isAvailable();
}
protected void finishConnectivity() {
Log.w("MmsService", "Calling stop using network feature!");
connectivityManager.stopUsingNetworkFeature(ConnectivityManager.TYPE_MOBILE, FEATURE_ENABLE_MMS);
if (connectivityListener != null) {
context.unregisterReceiver(connectivityListener);
connectivityListener = null;
}
if (this.wakeLock.isHeld())
this.wakeLock.release();
}
private void issueConnectivityChange() {
Intent intent = new Intent(context, SendReceiveService.class);
intent.setAction(getConnectivityAction());
context.startService(intent);
}
private class ConnectivityListener extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Log.w("MmsService", "Dispatching connectivity change...");
issueConnectivityChange();
Log.w("MmsService", "Dispatched...");
}
}
}

View File

@@ -0,0 +1,235 @@
/**
* Copyright (C) 2011 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.service;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.CanonicalSessionMigrator;
import org.thoughtcrime.securesms.util.WorkerThread;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import android.widget.Toast;
/**
* Services that handles sending/receiving of SMS/MMS.
*
* @author Moxie Marlinspike
*/
public class SendReceiveService extends Service {
public static final String SEND_SMS_ACTION = "org.thoughtcrime.securesms.SendReceiveService.SEND_SMS_ACTION";
public static final String SENT_SMS_ACTION = "org.thoughtcrime.securesms.SendReceiveService.SENT_SMS_ACTION";
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 SEND_MMS_CONNECTIVITY_ACTION = "org.thoughtcrime.securesms.SendReceiveService.SEND_MMS_CONNECTIVITY_ACTION";
public static final String RECEIVE_MMS_ACTION = "org.thoughtcrime.securesms.SendReceiveService.RECEIVE_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";
private static final int SEND_SMS = 0;
private static final int RECEIVE_SMS = 1;
private static final int SEND_MMS = 2;
private static final int RECEIVE_MMS = 3;
private static final int DOWNLOAD_MMS = 4;
private ToastHandler toastHandler;
private SmsReceiver smsReceiver;
private SmsSender smsSender;
private MmsReceiver mmsReceiver;
private MmsSender mmsSender;
private MmsDownloader mmsDownloader;
private MasterSecret masterSecret;
private NewKeyReceiver receiver;
private List<Runnable> workQueue;
private List<Runnable> pendingSecretList;
private Thread workerThread;
@Override
public void onCreate() {
initializeHandlers();
initializeProcessors();
initializeAddressCanonicalization();
initializeWorkQueue();
initializeMasterSecret();
}
@Override
public void onStart(Intent intent, int startId) {
if (intent.getAction().equals(SEND_SMS_ACTION))
scheduleSecretRequiredIntent(SEND_SMS, intent);
else if (intent.getAction().equals(RECEIVE_SMS_ACTION))
scheduleIntent(RECEIVE_SMS, intent);
else if (intent.getAction().equals(SENT_SMS_ACTION))
scheduleIntent(RECEIVE_SMS, intent);
else if (intent.getAction().equals(SEND_MMS_ACTION) || intent.getAction().equals(SEND_MMS_CONNECTIVITY_ACTION))
scheduleSecretRequiredIntent(SEND_MMS, intent);
else if (intent.getAction().equals(RECEIVE_MMS_ACTION))
scheduleIntent(RECEIVE_MMS, intent);
else if (intent.getAction().equals(DOWNLOAD_MMS_ACTION) || intent.getAction().equals(DOWNLOAD_MMS_CONNECTIVITY_ACTION))
scheduleSecretRequiredIntent(DOWNLOAD_MMS, intent);
else
Log.w("SendReceiveService", "Received intent with unknown action: " + intent.getAction());
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
private void initializeHandlers() {
toastHandler = new ToastHandler();
}
private void initializeProcessors() {
smsReceiver = new SmsReceiver(this);
smsSender = new SmsSender(this);
mmsReceiver = new MmsReceiver(this);
mmsSender = new MmsSender(this);
mmsDownloader = new MmsDownloader(this, toastHandler);
}
private void initializeWorkQueue() {
pendingSecretList = new LinkedList<Runnable>();
workQueue = new LinkedList<Runnable>();
workerThread = new WorkerThread(workQueue, "SendReceveService-WorkerThread");
workerThread.start();
}
private void initializeMasterSecret() {
receiver = new NewKeyReceiver();
IntentFilter filter = new IntentFilter(KeyCachingService.NEW_KEY_EVENT);
registerReceiver(receiver, filter, KeyCachingService.KEY_PERMISSION, null);
Intent bindIntent = new Intent(this, KeyCachingService.class);
bindService(bindIntent, serviceConnection, Context.BIND_AUTO_CREATE);
}
private void initializeWithMasterSecret(MasterSecret masterSecret) {
Log.w("SendReceiveService", "SendReceive service got master secret: " + masterSecret);
if (masterSecret != null) {
synchronized (workQueue) {
this.masterSecret = masterSecret;
Iterator<Runnable> iterator = pendingSecretList.iterator();
while (iterator.hasNext())
workQueue.add(iterator.next());
workQueue.notifyAll();
}
}
}
private void initializeAddressCanonicalization() {
CanonicalSessionMigrator.migrateSessions(this);
}
private void scheduleIntent(int what, Intent intent) {
Runnable work = new SendReceiveWorkItem(intent, what);
synchronized (workQueue) {
workQueue.add(work);
workQueue.notifyAll();
}
}
private void scheduleSecretRequiredIntent(int what, Intent intent) {
Runnable work = new SendReceiveWorkItem(intent, what);
synchronized (workQueue) {
if (masterSecret != null) {
workQueue.add(work);
workQueue.notifyAll();
} else {
pendingSecretList.add(work);
}
}
}
private class SendReceiveWorkItem implements Runnable {
private final Intent intent;
private final int what;
public SendReceiveWorkItem(Intent intent, int what) {
this.intent = intent;
this.what = what;
}
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;
}
}
}
public class ToastHandler extends Handler {
public void makeToast(String toast) {
Message message = this.obtainMessage();
message.obj = toast;
this.sendMessage(message);
}
@Override
public void handleMessage(Message message) {
Toast.makeText(SendReceiveService.this, (String)message.obj, Toast.LENGTH_LONG).show();
}
}
private ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder service) {
KeyCachingService keyCachingService = ((KeyCachingService.KeyCachingBinder)service).getService();
MasterSecret masterSecret = keyCachingService.getMasterSecret();
initializeWithMasterSecret(masterSecret);
SendReceiveService.this.unbindService(this);
}
public void onServiceDisconnected(ComponentName name) {}
};
private class NewKeyReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Log.w("SendReceiveService", "Got a MasterSecret broadcast...");
initializeWithMasterSecret((MasterSecret)intent.getParcelableExtra("master_secret"));
}
};
}

View File

@@ -0,0 +1,98 @@
/**
* Copyright (C) 2011 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.service;
import org.thoughtcrime.securesms.protocol.WirePrefix;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.telephony.SmsMessage;
import android.util.Log;
public class SmsListener extends BroadcastReceiver {
private static final String SMS_RECEIVED_ACTION = "android.provider.Telephony.SMS_RECEIVED";
private boolean isExemption(SmsMessage message, String messageBody) {
// Sprint Visual Voicemail
return
message.getOriginatingAddress().length() < 7 &&
(messageBody.startsWith("//ANDROID:") || messageBody.startsWith("//Android:") ||
messageBody.startsWith("//android:") || messageBody.startsWith("//BREW:"));
}
private SmsMessage getSmsMessageFromIntent(Intent intent) {
Bundle bundle = intent.getExtras();
Object[] pdus = (Object[])bundle.get("pdus");
if (pdus == null || pdus.length == 0)
return null;
return SmsMessage.createFromPdu((byte[])pdus[0]);
}
private String getSmsMessageBodyFromIntent(Intent intent) {
Bundle bundle = intent.getExtras();
Object[] pdus = (Object[])bundle.get("pdus");
StringBuilder bodyBuilder = new StringBuilder();
if (pdus == null)
return null;
for (int i=0;i<pdus.length;i++)
bodyBuilder.append(SmsMessage.createFromPdu((byte[])pdus[i]).getDisplayMessageBody());
return bodyBuilder.toString();
}
private boolean isRelevent(Context context, Intent intent) {
SmsMessage message = getSmsMessageFromIntent(intent);
String messageBody = getSmsMessageBodyFromIntent(intent);
if (message == null && messageBody == null)
return false;
if (isExemption(message, messageBody))
return false;
if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean("pref_all_sms", true))
return true;
return WirePrefix.isEncryptedMessage(messageBody) || WirePrefix.isKeyExchange(messageBody);
}
@Override
public void onReceive(Context context, Intent intent) {
Log.w("SMSListener", "Got SMS broadcast...");
if (intent.getAction().equals(SMS_RECEIVED_ACTION) && isRelevent(context, intent)) {
intent.setAction(SendReceiveService.RECEIVE_SMS_ACTION);
intent.putExtra("ResultCode", this.getResultCode());
intent.setClass(context, SendReceiveService.class);
context.startService(intent);
abortBroadcast();
} else if (intent.getAction().equals(SendReceiveService.SENT_SMS_ACTION)) {
intent.putExtra("ResultCode", this.getResultCode());
intent.setClass(context, SendReceiveService.class);
context.startService(intent);
}
}
}

View File

@@ -0,0 +1,185 @@
/**
* Copyright (C) 2011 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.service;
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
import org.thoughtcrime.securesms.crypto.DecryptingQueue;
import org.thoughtcrime.securesms.crypto.InvalidKeyException;
import org.thoughtcrime.securesms.crypto.InvalidVersionException;
import org.thoughtcrime.securesms.crypto.KeyExchangeMessage;
import org.thoughtcrime.securesms.crypto.KeyExchangeProcessor;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.protocol.Prefix;
import org.thoughtcrime.securesms.protocol.WirePrefix;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.sms.MultipartMessageHandler;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.telephony.SmsMessage;
import android.util.Log;
public class SmsReceiver {
private MultipartMessageHandler multipartMessageHandler = new MultipartMessageHandler();
private final Context context;
public SmsReceiver(Context context) {
this.context = context;
}
private String assembleSecureMessageFragments(String sender, String messageBody) {
String localPrefix;
if (WirePrefix.isEncryptedMessage(messageBody)) {
localPrefix = Prefix.ASYMMETRIC_ENCRYPT;
} else {
localPrefix = Prefix.KEY_EXCHANGE;
}
Log.w("SMSReceiverService", "Calculated local prefix for message: " + messageBody + " - Local Prefix: " + localPrefix);
messageBody = messageBody.substring(WirePrefix.PREFIX_SIZE);
Log.w("SMSReceiverService", "Parsed off wire prefix: " + messageBody);
if (!multipartMessageHandler.isManualTransport(messageBody))
return localPrefix + messageBody;
else
return multipartMessageHandler.processPotentialMultipartMessage(localPrefix, sender, messageBody);
}
private String assembleMessageFragments(SmsMessage[] messages) {
StringBuilder body = new StringBuilder();
for (SmsMessage message : messages) {
body.append(message.getDisplayMessageBody());
}
String messageBody = body.toString();
if (WirePrefix.isEncryptedMessage(messageBody) || WirePrefix.isKeyExchange(messageBody)) {
return assembleSecureMessageFragments(messages[0].getDisplayOriginatingAddress(), messageBody);
} else {
return messageBody;
}
}
private void storeSecureMessage(MasterSecret masterSecret, SmsMessage message, String messageBody) {
long messageId = DatabaseFactory.getSmsDatabase(context).insertSecureMessageReceived(message, messageBody);
Log.w("SmsReceiver", "Inserted secure message received: " + messageId);
if (masterSecret != null)
DecryptingQueue.scheduleDecryption(context, masterSecret, messageId, message.getDisplayOriginatingAddress(), messageBody);
}
private long storeStandardMessage(MasterSecret masterSecret, SmsMessage message, String messageBody) {
if (masterSecret != null) return DatabaseFactory.getEncryptingSmsDatabase(context).insertMessageReceived(masterSecret, message, messageBody);
else if (MasterSecretUtil.hasAsymmericMasterSecret(context)) return DatabaseFactory.getEncryptingSmsDatabase(context).insertMessageReceived(MasterSecretUtil.getAsymmetricMasterSecret(context, null), message, messageBody);
else return DatabaseFactory.getSmsDatabase(context).insertMessageReceived(message, messageBody);
}
private void storeKeyExchangeMessage(MasterSecret masterSecret, SmsMessage message, String messageBody) {
if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean(ApplicationPreferencesActivity.AUTO_KEY_EXCHANGE_PREF, true)) {
try {
Recipient recipient = new Recipient(null, message.getDisplayOriginatingAddress(), null);
KeyExchangeMessage keyExchangeMessage = new KeyExchangeMessage(messageBody);
KeyExchangeProcessor processor = new KeyExchangeProcessor(context, masterSecret, recipient);
Log.w("SmsReceiver", "Received key with fingerprint: " + keyExchangeMessage.getPublicKey().getFingerprint());
if (processor.isStale(keyExchangeMessage)) {
messageBody = messageBody.substring(Prefix.KEY_EXCHANGE.length());
messageBody = Prefix.STALE_KEY_EXCHANGE + messageBody;
} else if (!processor.hasCompletedSession() || processor.hasSameSessionIdentity(keyExchangeMessage)) {
messageBody = messageBody.substring(Prefix.KEY_EXCHANGE.length());
messageBody = Prefix.PROCESSED_KEY_EXCHANGE + messageBody;
long messageId = storeStandardMessage(masterSecret, message, messageBody);
long threadId = DatabaseFactory.getSmsDatabase(context).getThreadIdForMessage(messageId);
processor.processKeyExchangeMessage(keyExchangeMessage, threadId);
return;
}
} catch (InvalidVersionException e) {
Log.w("SmsReceiver", e);
} catch (InvalidKeyException e) {
Log.w("SmsReceiver", e);
}
}
storeStandardMessage(masterSecret, message, messageBody);
}
private boolean storeMessage(MasterSecret masterSecret, SmsMessage message, String messageBody) {
if (messageBody.startsWith(Prefix.ASYMMETRIC_ENCRYPT)) {
storeSecureMessage(masterSecret, message, messageBody);
} else if (messageBody.startsWith(Prefix.KEY_EXCHANGE)) {
storeKeyExchangeMessage(masterSecret, message, messageBody);
} else {
storeStandardMessage(masterSecret, message, messageBody);
}
return true;
}
private SmsMessage[] parseMessages(Bundle bundle) {
Object[] pdus = (Object[])bundle.get("pdus");
SmsMessage[] messages = new SmsMessage[pdus.length];
for (int i=0;i<pdus.length;i++)
messages[i] = SmsMessage.createFromPdu((byte[])pdus[i]);
return messages;
}
private void handleReceiveMessage(MasterSecret masterSecret, Intent intent) {
Bundle bundle = intent.getExtras();
SmsMessage[] messages = parseMessages(bundle);
String message = assembleMessageFragments(messages);
if (message != null) {
storeMessage(masterSecret, messages[0], message);
MessageNotifier.updateNotification(context, true);
}
}
private void handleSentMessage(Intent intent) {
long messageId = intent.getLongExtra("message_id", -1);
long type = intent.getLongExtra("type", -1);
Log.w("SMSReceiverService", "Intent resultcode: " + intent.getIntExtra("ResultCode", 42));
Log.w("SMSReceiverService", "Running sent callback: " + messageId + "," + type);
if (intent.getIntExtra("ResultCode", -31337) == Activity.RESULT_OK)
DatabaseFactory.getSmsDatabase(context).markAsSent(messageId, type);
else
DatabaseFactory.getSmsDatabase(context).markAsSentFailed(messageId);
}
public void process(MasterSecret masterSecret, Intent intent) {
if (intent.getAction().equals(SendReceiveService.RECEIVE_SMS_ACTION))
handleReceiveMessage(masterSecret, intent);
else if (intent.getAction().equals(SendReceiveService.SENT_SMS_ACTION))
handleSentMessage(intent);
}
}

View File

@@ -0,0 +1,183 @@
/**
* Copyright (C) 2011 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.service;
import java.util.ArrayList;
import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.SessionCipher;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.protocol.KeyExchangeWirePrefix;
import org.thoughtcrime.securesms.protocol.Prefix;
import org.thoughtcrime.securesms.protocol.SecureMessageWirePrefix;
import org.thoughtcrime.securesms.protocol.WirePrefix;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.sms.MultipartMessageHandler;
import org.thoughtcrime.securesms.sms.SmsTransportDetails;
import org.thoughtcrime.securesms.util.InvalidMessageException;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.telephony.SmsManager;
import android.util.Log;
public class SmsSender {
private final MultipartMessageHandler multipartMessageHandler = new MultipartMessageHandler();
private final Context context;
public SmsSender(Context context) {
this.context = context;
}
public void process(MasterSecret masterSecret, Intent intent) {
MasterCipher masterCipher = new MasterCipher(masterSecret);
long messageId = intent.getLongExtra("message_id", -1);
Cursor c = null;
Log.w("SMSSenderService", "Processing outgoing message: " + messageId);
try {
if (messageId == -1) c = DatabaseFactory.getSmsDatabase(context).getOutgoingMessages();
else c = DatabaseFactory.getSmsDatabase(context).getMessage(messageId);
if (c != null && c.moveToFirst()) {
do {
messageId = c.getLong(c.getColumnIndexOrThrow(SmsDatabase.ID));
String body = c.getString(c.getColumnIndexOrThrow(SmsDatabase.BODY));
String address = c.getString(c.getColumnIndexOrThrow(SmsDatabase.ADDRESS));
String messageText = getClearTextBody(masterCipher, body);
long type = c.getLong(c.getColumnIndexOrThrow(SmsDatabase.TYPE));
if (!SmsDatabase.Types.isPendingMessageType(type))
continue;
if (isSecureMessage(type))
messageText = getAsymmetricEncrypt(masterSecret, messageText, address);
Log.w("SMSSenderService", "Actually delivering: " + messageId);
deliverTextMessage(address, messageText, messageId, type);
} while (c.moveToNext());
}
} finally {
if (c != null)
c.close();
}
}
private String getClearTextBody(MasterCipher masterCipher, String body) {
if (body.startsWith(Prefix.SYMMETRIC_ENCRYPT)) {
try {
return masterCipher.decryptBody(body.substring(Prefix.SYMMETRIC_ENCRYPT.length()));
} catch (InvalidMessageException e) {
return "Error decrypting message.";
}
} else {
return body;
}
}
private ArrayList<PendingIntent> constructSentIntents(long messageId, long type, ArrayList<String> messages) {
ArrayList<PendingIntent> sentIntents = new ArrayList<PendingIntent>(messages.size());
for (int i=0;i<messages.size();i++) {
Intent pending = new Intent(SendReceiveService.SENT_SMS_ACTION, Uri.parse("custom://" + messageId + System.currentTimeMillis()), context, SmsListener.class);
pending.putExtra("type", type);
pending.putExtra("message_id", messageId);
sentIntents.add(PendingIntent.getBroadcast(context, 0, pending, 0));
}
return sentIntents;
}
private void deliverGSMTransportTextMessage(String recipient, String text, long messageId, long type) {
ArrayList<String> messages = SmsManager.getDefault().divideMessage(text);
ArrayList<PendingIntent> sentIntents = constructSentIntents(messageId, type, messages);
// XXX moxie@thoughtcrime.org 1/7/11 -- There's apparently a bug where for some unknown recipients
// and messages, this will throw an NPE. I have no idea why, so I'm just catching it and marking
// the message as a failure. That way at least it doesn't repeatedly crash every time you start
// the app.
try {
SmsManager.getDefault().sendMultipartTextMessage(recipient, null, messages, sentIntents, null);
} catch (NullPointerException npe) {
Log.w("SmsSender", npe);
DatabaseFactory.getSmsDatabase(context).markAsSentFailed(messageId);
}
}
private void deliverSecureTransportTextMessage(String recipient, String text, long messageId, long type) {
WirePrefix prefix;
if (isSecureMessage(type)) {
prefix = new SecureMessageWirePrefix();
text = text.substring(Prefix.ASYMMETRIC_ENCRYPT.length());
} else {
prefix = new KeyExchangeWirePrefix();
text = text.substring(Prefix.KEY_EXCHANGE.length());
}
if (!multipartMessageHandler.isManualTransport(text)) {
deliverGSMTransportTextMessage(recipient, prefix.calculatePrefix(text) + text, messageId, type);
return;
}
ArrayList<String> messages = multipartMessageHandler.divideMessage(recipient, text, prefix);
ArrayList<PendingIntent> sentIntents = constructSentIntents(messageId, type, messages);
for (int i=0;i<messages.size();i++) {
// XXX moxie@thoughtcrime.org 1/7/11 -- There's apparently a bug where for some unknown recipients
// and messages, this will throw an NPE. I have no idea why, so I'm just catching it and marking
// the message as a failure. That way at least it doesn't repeatedly crash every time you start
// the app.
try {
SmsManager.getDefault().sendTextMessage(recipient, null, messages.get(i), sentIntents.get(i), null);
} catch (NullPointerException npe) {
Log.w("SmsSender", npe);
DatabaseFactory.getSmsDatabase(context).markAsSentFailed(messageId);
}
}
}
private void deliverTextMessage(String recipient, String text, long messageId, long type) {
if (!isSecureMessage(type) && !isKeyExchange(text))
deliverGSMTransportTextMessage(recipient, text, messageId, type);
else
deliverSecureTransportTextMessage(recipient, text, messageId, type);
}
private boolean isSecureMessage(long type) {
return type == SmsDatabase.Types.ENCRYPTING_TYPE;
}
private boolean isKeyExchange(String messageText) {
return messageText.startsWith(Prefix.KEY_EXCHANGE);
}
private String getAsymmetricEncrypt(MasterSecret masterSecret, String body, String address) {
synchronized (SessionCipher.CIPHER_LOCK) {
SessionCipher cipher = new SessionCipher(context, masterSecret, new Recipient(null, address, null), new SmsTransportDetails());
return new String(cipher.encryptMessage(body.getBytes()));
}
}
}