/** * 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 com.securecomcode.text.service; 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.Message; import android.util.Log; import android.widget.Toast; import com.securecomcode.text.crypto.InvalidPassphraseException; import com.securecomcode.text.crypto.MasterSecretUtil; import com.securecomcode.text.database.CanonicalSessionMigrator; import com.securecomcode.text.util.TextSecurePreferences; import com.securecomcode.text.util.WorkerThread; import org.whispersystems.textsecure.crypto.MasterSecret; import java.util.Iterator; import java.util.LinkedList; import java.util.List; /** * Services that handles sending/receiving of SMS/MMS. * * @author Moxie Marlinspike */ public class SendReceiveService extends Service { public static final String SEND_SMS_ACTION = "com.securecomcode.text.SendReceiveService.SEND_SMS_ACTION"; public static final String SENT_SMS_ACTION = "com.securecomcode.text.SendReceiveService.SENT_SMS_ACTION"; public static final String DELIVERED_SMS_ACTION = "com.securecomcode.text.SendReceiveService.DELIVERED_SMS_ACTION"; public static final String RECEIVE_SMS_ACTION = "com.securecomcode.text.SendReceiveService.RECEIVE_SMS_ACTION"; public static final String SEND_MMS_ACTION = "com.securecomcode.text.SendReceiveService.SEND_MMS_ACTION"; public static final String RECEIVE_MMS_ACTION = "com.securecomcode.text.SendReceiveService.RECEIVE_MMS_ACTION"; public static final String DOWNLOAD_MMS_ACTION = "com.securecomcode.text.SendReceiveService.DOWNLOAD_MMS_ACTION"; public static final String DOWNLOAD_MMS_CONNECTIVITY_ACTION = "com.securecomcode.text.SendReceiveService.DOWNLOAD_MMS_CONNECTIVITY_ACTION"; public static final String DOWNLOAD_MMS_PENDING_APN_ACTION = "com.securecomcode.text.SendReceiveService.DOWNLOAD_MMS_PENDING_APN_ACTION"; public static final String RECEIVE_PUSH_ACTION = "com.securecomcode.text.SendReceiveService.RECEIVE_PUSH_ACTION"; public static final String DECRYPTED_PUSH_ACTION = "com.securecomcode.text.SendReceiveService.DECRYPTED_PUSH_ACTION"; public static final String DOWNLOAD_PUSH_ACTION = "com.securecomcode.text.SendReceiveService.DOWNLOAD_PUSH_ACTION"; public static final String DOWNLOAD_AVATAR_ACTION = "com.securecomcode.text.SendReceiveService.DOWNLOAD_AVATAR_ACTION"; public static final String MASTER_SECRET_EXTRA = "master_secret"; 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 static final int DOWNLOAD_MMS_PENDING = 5; private static final int RECEIVE_PUSH = 6; private static final int DOWNLOAD_PUSH = 7; private static final int DOWNLOAD_AVATAR = 8; private ToastHandler toastHandler; private SystemStateListener systemStateListener; private SmsReceiver smsReceiver; private SmsSender smsSender; private MmsReceiver mmsReceiver; private MmsSender mmsSender; private MmsDownloader mmsDownloader; private PushReceiver pushReceiver; private PushDownloader pushDownloader; private AvatarDownloader avatarDownloader; private MasterSecret masterSecret; private boolean hasSecret; private NewKeyReceiver newKeyReceiver; private ClearKeyReceiver clearKeyReceiver; private List<Runnable> workQueue; private List<Runnable> pendingSecretList; @Override public void onCreate() { initializeHandlers(); initializeProcessors(); initializeAddressCanonicalization(); initializeWorkQueue(); initializeMasterSecret(); } @Override public void onStart(Intent intent, int startId) { if (intent == null) return; String action = intent.getAction(); if (action.equals(SEND_SMS_ACTION)) scheduleSecretRequiredIntent(SEND_SMS, intent); else if (action.equals(RECEIVE_SMS_ACTION)) scheduleIntent(RECEIVE_SMS, intent); else if (action.equals(SENT_SMS_ACTION)) scheduleIntent(SEND_SMS, intent); else if (action.equals(DELIVERED_SMS_ACTION)) scheduleIntent(SEND_SMS, intent); else if (action.equals(SEND_MMS_ACTION)) scheduleSecretRequiredIntent(SEND_MMS, intent); 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 if (action.equals(DOWNLOAD_AVATAR_ACTION)) scheduleIntent(DOWNLOAD_AVATAR, intent); else Log.w("SendReceiveService", "Received intent with unknown action: " + intent.getAction()); } @Override public IBinder onBind(Intent intent) { return null; } @Override public void onDestroy() { Log.w("SendReceiveService", "onDestroy()..."); super.onDestroy(); if (newKeyReceiver != null) unregisterReceiver(newKeyReceiver); if (clearKeyReceiver != null) unregisterReceiver(clearKeyReceiver); } private void initializeHandlers() { systemStateListener = new SystemStateListener(this); toastHandler = new ToastHandler(); } private void initializeProcessors() { smsReceiver = new SmsReceiver(this); smsSender = new SmsSender(this, systemStateListener, toastHandler); mmsReceiver = new MmsReceiver(this); mmsSender = new MmsSender(this, systemStateListener, toastHandler); mmsDownloader = new MmsDownloader(this, toastHandler); pushReceiver = new PushReceiver(this); pushDownloader = new PushDownloader(this); avatarDownloader = new AvatarDownloader(this); } private void initializeWorkQueue() { pendingSecretList = new LinkedList<Runnable>(); workQueue = new LinkedList<Runnable>(); Thread workerThread = new WorkerThread(workQueue, "SendReceveService-WorkerThread"); workerThread.start(); } private void initializeMasterSecret() { hasSecret = false; newKeyReceiver = new NewKeyReceiver(); clearKeyReceiver = new ClearKeyReceiver(); IntentFilter newKeyFilter = new IntentFilter(KeyCachingService.NEW_KEY_EVENT); registerReceiver(newKeyReceiver, newKeyFilter, KeyCachingService.KEY_PERMISSION, null); IntentFilter clearKeyFilter = new IntentFilter(KeyCachingService.CLEAR_KEY_EVENT); registerReceiver(clearKeyReceiver, clearKeyFilter, KeyCachingService.KEY_PERMISSION, null); Intent bindIntent = new Intent(this, KeyCachingService.class); startService(bindIntent); bindService(bindIntent, serviceConnection, Context.BIND_AUTO_CREATE); } private void initializeWithMasterSecret(MasterSecret masterSecret) { Log.w("SendReceiveService", "SendReceive service got master secret."); if (masterSecret != null) { synchronized (workQueue) { this.masterSecret = masterSecret; this.hasSecret = true; Iterator<Runnable> iterator = pendingSecretList.iterator(); while (iterator.hasNext()) workQueue.add(iterator.next()); workQueue.notifyAll(); } } } private void initializeAddressCanonicalization() { CanonicalSessionMigrator.migrateSessions(this); } private MasterSecret getPlaceholderSecret() { try { return MasterSecretUtil.getMasterSecret(SendReceiveService.this, MasterSecretUtil.UNENCRYPTED_PASSPHRASE); } catch (InvalidPassphraseException e) { Log.w("SendReceiveService", e); return null; } } 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 (!hasSecret && TextSecurePreferences.isPasswordDisabled(SendReceiveService.this)) { initializeWithMasterSecret(getPlaceholderSecret()); } if (hasSecret) { 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; } @Override public void run() { MasterSecret masterSecret = SendReceiveService.this.masterSecret; if (masterSecret == null && TextSecurePreferences.isPasswordDisabled(SendReceiveService.this)) { masterSecret = getPlaceholderSecret(); } 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 DOWNLOAD_MMS_PENDING: mmsDownloader.process(masterSecret, intent); return; case RECEIVE_PUSH: pushReceiver.process(masterSecret, intent); return; case DOWNLOAD_PUSH: pushDownloader.process(masterSecret, intent); return; case DOWNLOAD_AVATAR: avatarDownloader.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() { @Override public void onServiceConnected(ComponentName className, IBinder service) { KeyCachingService keyCachingService = ((KeyCachingService.KeyCachingBinder)service).getService(); MasterSecret masterSecret = keyCachingService.getMasterSecret(); initializeWithMasterSecret(masterSecret); SendReceiveService.this.unbindService(this); } @Override 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_EXTRA)); } } /** * This class receives broadcast notifications to clear the MasterSecret. * * We don't want to clear it immediately, since there are potentially jobs * in the work queue which require the master secret. Instead, we reset a * flag so that new incoming jobs will be evaluated as if no mastersecret is * present. * * Then, we add a job to the end of the queue which actually clears the masterSecret * value. That way all jobs before this moment will be processed correctly, and all * jobs after this moment will be evaluated as if no mastersecret is present (and potentially * held). * * When we go to actually clear the mastersecret, we ensure that the flag is still false. * This allows a new mastersecret broadcast to come in correctly without us clobbering it. * */ private class ClearKeyReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { Log.w("SendReceiveService", "Got a clear mastersecret broadcast..."); synchronized (workQueue) { SendReceiveService.this.hasSecret = false; workQueue.add(new Runnable() { @Override public void run() { Log.w("SendReceiveService", "Running clear key work item..."); synchronized (workQueue) { if (!SendReceiveService.this.hasSecret) { Log.w("SendReceiveService", "Actually clearing key..."); SendReceiveService.this.masterSecret = null; } } } }); workQueue.notifyAll(); } } }; }