/** * Copyright (C) 2013 Open Whisper Systems * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.thoughtcrime.securesms.transport; import android.app.PendingIntent; import android.content.Context; import android.telephony.SmsManager; import android.util.Log; import org.thoughtcrime.securesms.database.model.SmsMessageRecord; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.sms.MultipartSmsMessageHandler; import org.thoughtcrime.securesms.sms.OutgoingPrekeyBundleMessage; import org.thoughtcrime.securesms.sms.OutgoingTextMessage; import org.thoughtcrime.securesms.sms.SmsTransportDetails; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.textsecure.crypto.MasterSecret; import org.whispersystems.textsecure.crypto.SessionCipher; import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage; import org.whispersystems.textsecure.storage.RecipientDevice; import org.whispersystems.textsecure.util.Hex; import java.util.ArrayList; public class SmsTransport extends BaseTransport { private final Context context; private final MasterSecret masterSecret; public SmsTransport(Context context, MasterSecret masterSecret) { this.context = context.getApplicationContext(); this.masterSecret = masterSecret; } public void deliver(SmsMessageRecord message) throws UndeliverableMessageException { if (message.isSecure() || message.isKeyExchange() || message.isEndSession()) { deliverSecureMessage(message); } else { deliverPlaintextMessage(message); } } private void deliverSecureMessage(SmsMessageRecord message) throws UndeliverableMessageException { MultipartSmsMessageHandler multipartMessageHandler = new MultipartSmsMessageHandler(); OutgoingTextMessage transportMessage = OutgoingTextMessage.from(message); if (message.isSecure() || message.isEndSession()) { transportMessage = getAsymmetricEncrypt(masterSecret, transportMessage); } ArrayList messages = multipartMessageHandler.divideMessage(transportMessage); ArrayList sentIntents = constructSentIntents(message.getId(), message.getType(), messages, message.isSecure()); ArrayList deliveredIntents = constructDeliveredIntents(message.getId(), message.getType(), messages); Log.w("SmsTransport", "Secure divide into message parts: " + messages.size()); for (int i=0;i messages = SmsManager.getDefault().divideMessage(message.getBody().getBody()); ArrayList sentIntents = constructSentIntents(message.getId(), message.getType(), messages, false); ArrayList deliveredIntents = constructDeliveredIntents(message.getId(), message.getType(), messages); String recipient = message.getIndividualRecipient().getNumber(); // 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. // d3sre 12/10/13 -- extended the log file to further analyse the problem try { SmsManager.getDefault().sendMultipartTextMessage(recipient, null, messages, sentIntents, deliveredIntents); } catch (NullPointerException npe) { Log.w("SmsTransport", npe); Log.w("SmsTransport", "Recipient: " + recipient); Log.w("SmsTransport", "Message Parts: " + messages.size()); try { for (int i=0;i constructSentIntents(long messageId, long type, ArrayList messages, boolean secure) { ArrayList sentIntents = new ArrayList(messages.size()); for (String ignored : messages) { sentIntents.add(PendingIntent.getBroadcast(context, 0, constructSentIntent(context, messageId, type, secure), 0)); } return sentIntents; } private ArrayList constructDeliveredIntents(long messageId, long type, ArrayList messages) { if (!TextSecurePreferences.isSmsDeliveryReportsEnabled(context)) { return null; } ArrayList deliveredIntents = new ArrayList(messages.size()); for (String ignored : messages) { deliveredIntents.add(PendingIntent.getBroadcast(context, 0, constructDeliveredIntent(context, messageId, type), 0)); } return deliveredIntents; } private OutgoingTextMessage getAsymmetricEncrypt(MasterSecret masterSecret, OutgoingTextMessage message) { Recipient recipient = message.getRecipients().getPrimaryRecipient(); RecipientDevice recipientDevice = new RecipientDevice(recipient.getRecipientId(), RecipientDevice.DEFAULT_DEVICE_ID); String body = message.getMessageBody(); SmsTransportDetails transportDetails = new SmsTransportDetails(); SessionCipher sessionCipher = SessionCipher.createFor(context, masterSecret, recipientDevice); byte[] paddedPlaintext = transportDetails.getPaddedMessageBody(body.getBytes()); CiphertextMessage ciphertextMessage = sessionCipher.encrypt(paddedPlaintext); String encodedCiphertext = new String(transportDetails.getEncodedMessage(ciphertextMessage.serialize())); if (ciphertextMessage.getType() == CiphertextMessage.PREKEY_WHISPER_TYPE) { message = new OutgoingPrekeyBundleMessage(message, encodedCiphertext); } else { message = message.withBody(encodedCiphertext); } return message; } }