/** * Copyright (C) 2013 Jonathan Gillett, Joseph Heron * * 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.tinfoil.sms.utility; import org.strippedcastle.crypto.InvalidCipherTextException; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.media.Ringtone; import android.media.RingtoneManager; import android.net.Uri; import android.os.Bundle; import android.os.Vibrator; import android.preference.PreferenceManager; import android.telephony.SmsMessage; import android.util.Log; import android.widget.Toast; import com.bugsense.trace.BugSenseHandler; import com.tinfoil.sms.R; import com.tinfoil.sms.crypto.Encryption; import com.tinfoil.sms.crypto.KeyExchange; import com.tinfoil.sms.crypto.KeyExchangeHandler; import com.tinfoil.sms.dataStructures.Entry; import com.tinfoil.sms.dataStructures.Message; import com.tinfoil.sms.dataStructures.Number; import com.tinfoil.sms.database.DBAccessor; import com.tinfoil.sms.settings.QuickPrefsActivity; import com.tinfoil.sms.sms.ConversationView; import com.tinfoil.sms.sms.KeyExchangeManager; public class MessageReceiver extends BroadcastReceiver { public static boolean myActivityStarted = false; public static boolean keyExchangeManual = false; public static boolean keyExchange = false; public static boolean invalidKeyExchange = false; public static final String VIBRATOR_LENTH = "500"; private static OnKeyExchangeResolvedListener listener; private static final String ACTION_SMS_RECEIVED = "android.provider.Telephony.SMS_RECEIVED"; private DBAccessor dba; public void setOnKeyExchangeResolvedListener( OnKeyExchangeResolvedListener newListener) { listener = newListener; } public void removeOnKeyExchangeResolvedListener() { listener = null; } @Override public void onReceive(Context context, Intent intent) { dba = new DBAccessor(context); Bundle bundle = intent.getExtras(); if (intent.getAction().equals(ACTION_SMS_RECEIVED) && bundle != null) { /* * This will put every new message into a array of * SmsMessages. The message is received as a pdu, * and needs to be converted to a SmsMessage, if you want to * get information about the message. */ Object[] pdus = (Object[]) bundle.getSerializable("pdus"); if (pdus != null) { keyExchangeManual = false; SmsMessage[] messages = new SmsMessage[pdus.length]; String fullMessage = ""; //TODO handle mms data for (int i = 0; i < pdus.length; i++) { messages[i] = SmsMessage.createFromPdu((byte[]) pdus[i]); fullMessage += messages[i].getMessageBody(); } Log.v("Message", fullMessage.toString()); if (messages.length > -1) { /* * Checks if the database interface has been initialized and * if tinfoil-sms's preference interface has been dealt with */ if (dba == null || ConversationView.sharedPrefs == null) { dba = new DBAccessor(context); ConversationView.sharedPrefs = PreferenceManager .getDefaultSharedPreferences(context); } final String address = messages[0].getOriginatingAddress(); String secretMessage = null; /* * Checks if the contact is in the database */ //TODO re-think this for 4.4 (if Tinfoil-SMS is default then it should prob. catch all messages). if (address != null && dba.inDatabase(address)) { handleNotifSound(context); invalidKeyExchange = false; secretMessage = handleEncryptedMessage(context, address, fullMessage); if(secretMessage == null) { Log.v("message", fullMessage); /* * Since the user is not trusted, the message could * be a key exchange. Assume it is check for key * exchange message Only once it fails is the * message considered plain text. */ if(!handleKeyExchange(context, address, fullMessage)) { handlePlainMessage(context, address, fullMessage); } } /* * Update the list of messages to show the new messages */ ConversationView.updateList(context, ConversationView.messageViewActive); handleNotification(context, address, fullMessage, secretMessage); // Prevent other applications from seeing the message // received this.abortBroadcast(); } } } } } private boolean handleKeyExchange(Context context, final String address, final String fullMessage) { Number number = dba.getNumber(SMSUtility.format(address)); if (number.getKeyExchangeFlag() != Number.IGNORE && KeyExchange.isKeyExchange(fullMessage)) { if ((number.getKeyExchangeFlag() == Number.AUTO || (number.getKeyExchangeFlag() == Number.MANUAL && number.isInitiator())) && SMSUtility.checksharedSecret(number.getSharedInfo1()) && SMSUtility.checksharedSecret(number.getSharedInfo2())) { // Handle the key exchange received new KeyExchangeHandler(context, number, fullMessage, false) { @Override public void accept() { keyExchange = true; Toast.makeText(this.getContext(), R.string.key_exchange_received, Toast.LENGTH_SHORT).show(); Log.v("Key Exchange", "Exchange Key Message Received"); Log.v("S1", this.getNumber().getSharedInfo1()); Log.v("S2", this.getNumber().getSharedInfo2()); this.getNumber().setPublicKey(KeyExchange.encodedPubKey(this .getSignedPubKey())); this.getNumber().setSignature(KeyExchange.encodedSignature(this .getSignedPubKey())); dba.updateNumberRow(this.getNumber(), this .getNumber().getNumber(), 0); if (!this.getNumber().isInitiator()) { Log.v("Key Exchange", "Not Initiator"); String keyMessage = KeyExchange.sign(this.getNumber(), dba, SMSUtility.user); dba.addMessageToQueue(getNumber().getNumber(), keyMessage, true); //Store the key exchange in message list (received key exchange, not initiator) Message receivedMessage = new Message(fullMessage, true, Message.RECEIVED_KEY_EXCHANGE_RESP); dba.addNewMessage(receivedMessage, address, true); Message newMessage = new Message(keyMessage, true, Message.SENT_KEY_EXCHANGE_RESP); dba.addNewMessage(newMessage, address, true); } else { //Store the key exchange in message list (received key exchange, initiator) Message newMessage = new Message(fullMessage, true, Message.RECEIVED_KEY_EXCHANGE_INIT); dba.addNewMessage(newMessage, address, true); } if(listener != null) { Log.v("onKeyExchangeResolved", "TRUE, RECEIVED"); listener.onKeyExchangeResolved(); } super.accept(); } @Override public void invalid() { invalidKeyExchange = true; super.invalid(); } public void store() { cancel(); } public void cancel() { //Handle a key exchange that was initiated by contact however user has already initiated one. keyExchangeManual = true; Toast.makeText(this.getContext(), R.string.key_exchange_received, Toast.LENGTH_SHORT).show(); Log.v("Key Exchange", "Manual"); String result = dba.addKeyExchangeMessage(new Entry(address, this.getSignedPubKey())); //Store the key exchange in message list Message newMessage = new Message(fullMessage, true, Message.RECEIVED_KEY_EXCHANGE_INIT_RESP); dba.addNewMessage(newMessage, address, true); if (result != null) { Toast.makeText(this.getContext(), result, Toast.LENGTH_LONG).show(); } KeyExchangeManager.updateList(); MessageService.contentTitle = null; MessageService.contentText = null; Intent serviceIntent = new Intent(this.getContext(), MessageService.class); this.getContext().startService(serviceIntent); super.cancel(); } @Override public void finishWith() { } }; // Might be good to condense this into a // method. /* * if(KeyExchange.verify(number, message)) { * * } else { invalidKeyExchange = true; } */ } else { keyExchangeManual = true; Toast.makeText(context, R.string.key_exchange_received, Toast.LENGTH_SHORT).show(); Log.v("Key Exchange", "Manual"); String result = dba.addKeyExchangeMessage(new Entry(address, fullMessage)); //TODO determine which type of key exchange it is Message newMessage = new Message(fullMessage, true, Message.RECEIVED_KEY_EXCHANGE_INIT); dba.addNewMessage(newMessage, address, true); if (result != null) { Toast.makeText(context, result, Toast.LENGTH_LONG).show(); } KeyExchangeManager.updateList(); } return true; } return false; } private void handlePlainMessage(Context context, String address, String fullMessage) { /* * Send and store a plain text message to the * contact */ SMSUtility.sendToSelf(context, address, fullMessage, ConversationView.INBOX); Message newMessage = new Message(fullMessage, true, Message.RECEIVED_DEFAULT); dba.addNewMessage(newMessage, address, true); } private String handleEncryptedMessage(Context context, String address, String fullMessage) { String secretMessage = null; /* * Checks if the user is a trusted contact and if * tinfoil-sms encryption is enabled. */ if (dba.isTrustedContact((address)) && ConversationView.sharedPrefs.getBoolean( QuickPrefsActivity.ENABLE_SETTING_KEY, true)) { Message encryMessage = null; /* * Since contact is trusted assume it is NOT a key * exchange and that the message IS encrypted. If * the message fails to decrypt. A warning of * possible Man-In-The-Middle attack is given. */ try { /* * Now send the decrypted message to ourself, * set the source address of the message to the * original sender of the message */ SMSUtility.sendToSelf(context, address, fullMessage, ConversationView.INBOX); // Updates the last message received Message newMessage = null; Log.v("Before Decryption", fullMessage); // Initialize the cryptographic engine if null if (SMSUtility.cryptoEngine == null) { SMSUtility.cryptoEngine = new Encryption(SMSUtility.getUser(dba, null)); } Number contactNumber = dba.getNumber(SMSUtility.format(address)); secretMessage = SMSUtility.cryptoEngine.decrypt(contactNumber, fullMessage); Log.v("After Decryption", secretMessage); dba.updateDecryptNonce(contactNumber); /* * Checks if the user has set encrypted messages * to be shown in messageView */ if (ConversationView.sharedPrefs.getBoolean(QuickPrefsActivity .SHOW_ENCRYPT_SETTING_KEY, false)) { encryMessage = new Message(fullMessage, true, Message.RECEIVED_ENCRYPTED); dba.addNewMessage(encryMessage, address, true); } SMSUtility.sendToSelf(context, address, secretMessage, ConversationView.INBOX); /* * Store the message in the database */ newMessage = new Message(secretMessage, true, Message.RECEIVED_ENCRYPTED); dba.addNewMessage(newMessage, address, true); } catch (InvalidCipherTextException e) { encryMessage = new Message(fullMessage, true, Message.RECEIVED_ENCRYPT_FAIL); dba.addNewMessage(encryMessage, address, true); Toast.makeText(context, R.string.key_exchange_failed_to_decrypt, Toast.LENGTH_LONG).show(); Toast.makeText(context, R.string.possible_man_in_the_middle_attack_warning, Toast.LENGTH_LONG).show(); e.printStackTrace(); BugSenseHandler.sendExceptionMessage("Type", "Decrypt Message Error or Man In The Middle Attack", e); } catch (Exception e) { e.printStackTrace(); BugSenseHandler.sendExceptionMessage("Type", "Message Receiver Error", e); } } return secretMessage; } private void handleNotifSound(Context context) { if(SMSUtility.checkDefault(context)) { /* * Checks if the user has enabled the vibration option */ if (ConversationView.sharedPrefs.getBoolean( QuickPrefsActivity.VIBRATE_SETTING_KEY, true)) { Vibrator vibrator; vibrator = (Vibrator) context .getSystemService(Context.VIBRATOR_SERVICE); String value = ConversationView.sharedPrefs .getString( QuickPrefsActivity.VIBRATE_LENGTH_SETTING_KEY, VIBRATOR_LENTH); vibrator.vibrate(Long.valueOf(value)); } if (ConversationView.sharedPrefs.getBoolean( QuickPrefsActivity.RINGTONE_SETTING_KEY, false)) { Uri notification = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION); if(notification == null){ notification = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE); } Ringtone ringtone = RingtoneManager.getRingtone(context, notification); if(ringtone != null) { ringtone.play(); } } } } private void handleNotification(Context context, String address, String message, String secretMessage) { if(SMSUtility.checkDefault(context)) { // Check if the message was an invalid key exchange if (!invalidKeyExchange) { // Check if there should be a key exchange // notification if (!keyExchange) { if (!keyExchangeManual) { /* * Set the values needed for the * notification */ MessageService.contentTitle = SMSUtility .format(address); if (secretMessage != null) { MessageService.contentText = secretMessage; } else { MessageService.contentText = message; } } else { MessageService.contentTitle = null; MessageService.contentText = null; } Intent serviceIntent = new Intent(context, MessageService.class); context.startService(serviceIntent); } } } } }