Support encrypted transport, properly handle multiple recipients.

1) Add encryption support for the transport layer.  This obscures
   metadata from the push messaging provider.

2) Better support the direction multiple destination messages is
   headed (one unique message per recipient).
This commit is contained in:
Moxie Marlinspike
2013-08-29 17:01:30 -07:00
parent 68ec0a3727
commit 0cc5837d7f
17 changed files with 1798 additions and 107 deletions

View File

@@ -5,11 +5,12 @@ import android.content.Intent;
import android.util.Log;
import com.google.android.gcm.GCMBaseIntentService;
import com.google.thoughtcrimegson.Gson;
import org.thoughtcrime.securesms.service.RegistrationService;
import org.thoughtcrime.securesms.service.SendReceiveService;
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.textsecure.crypto.InvalidVersionException;
import org.whispersystems.textsecure.push.IncomingEncryptedPushMessage;
import org.whispersystems.textsecure.push.IncomingPushMessage;
import org.whispersystems.textsecure.push.PushServiceSocket;
import org.whispersystems.textsecure.util.Util;
@@ -48,16 +49,24 @@ public class GcmIntentService extends GCMBaseIntentService {
@Override
protected void onMessage(Context context, Intent intent) {
String data = intent.getStringExtra("message");
Log.w("GcmIntentService", "GCM message: " + data);
try {
String data = intent.getStringExtra("message");
Log.w("GcmIntentService", "GCM message: " + data);
if (Util.isEmpty(data))
return;
if (Util.isEmpty(data))
return;
IncomingPushMessage message = new Gson().fromJson(data, IncomingPushMessage.class);
String sessionKey = TextSecurePreferences.getSignalingKey(context);
IncomingEncryptedPushMessage encryptedMessage = new IncomingEncryptedPushMessage(data, sessionKey);
IncomingPushMessage message = encryptedMessage.getIncomingPushMessage();
if (!message.hasAttachments()) handleIncomingTextMessage(context, message);
else handleIncomingMediaMessage(context, message);
if (!message.hasAttachments()) handleIncomingTextMessage(context, message);
else handleIncomingMediaMessage(context, message);
} catch (IOException e) {
Log.w("GcmIntentService", e);
} catch (InvalidVersionException e) {
Log.w("GcmIntentService", e);
}
}
@Override

View File

@@ -42,6 +42,7 @@ import org.whispersystems.textsecure.crypto.InvalidVersionException;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.crypto.protocol.PreKeyBundleMessage;
import org.whispersystems.textsecure.push.OutgoingPushMessage;
import org.whispersystems.textsecure.push.PushMessage;
import org.whispersystems.textsecure.storage.InvalidKeyIdException;
import java.util.List;
@@ -62,11 +63,11 @@ public class SmsReceiver {
IncomingTextMessage message = messages.get(0);
switch (pushType) {
case OutgoingPushMessage.TYPE_MESSAGE_CIPHERTEXT:
case PushMessage.TYPE_MESSAGE_CIPHERTEXT:
return new IncomingEncryptedMessage(message, message.getMessageBody());
case OutgoingPushMessage.TYPE_MESSAGE_PREKEY_BUNDLE:
case PushMessage.TYPE_MESSAGE_PREKEY_BUNDLE:
return new IncomingPreKeyBundleMessage(message, message.getMessageBody());
case OutgoingPushMessage.TYPE_MESSAGE_KEY_EXCHANGE:
case PushMessage.TYPE_MESSAGE_KEY_EXCHANGE:
return new IncomingKeyExchangeMessage(message, message.getMessageBody());
}

View File

@@ -55,7 +55,7 @@ public class PushTransport extends BaseTransport {
String recipientCanonicalNumber = PhoneNumberFormatter.formatNumber(recipient.getNumber(),
localNumber);
Pair<Integer, String> typeAndCiphertext = getEncryptedMessage(socket, recipient, recipientCanonicalNumber, plaintext);
Pair<Integer, byte[]> typeAndCiphertext = getEncryptedMessage(socket, recipient, recipientCanonicalNumber, plaintext);
socket.sendMessage(recipientCanonicalNumber, typeAndCiphertext.second, typeAndCiphertext.first);
@@ -71,11 +71,19 @@ public class PushTransport extends BaseTransport {
String localNumber = TextSecurePreferences.getLocalNumber(context);
String password = TextSecurePreferences.getPushServerPassword(context);
PushServiceSocket socket = new PushServiceSocket(context, localNumber, password);
String messageText = PartParser.getMessageText(message.getBody());
byte[] messageText = PartParser.getMessageText(message.getBody()).getBytes();
List<PushAttachmentData> attachments = getAttachmentsFromBody(message.getBody());
if (attachments.isEmpty()) socket.sendMessage(destinations, messageText, OutgoingPushMessage.TYPE_MESSAGE_PLAINTEXT);
else socket.sendMessage(destinations, messageText, attachments, OutgoingPushMessage.TYPE_MESSAGE_PLAINTEXT);
List<byte[]> messagesList = new LinkedList<byte[]>();
List<List<PushAttachmentData>> attachmentsList = new LinkedList<List<PushAttachmentData>>();
for (String recipient : destinations) {
messagesList.add(messageText);
attachmentsList.add(attachments);
}
socket.sendMessage(destinations, messagesList, attachmentsList,
OutgoingPushMessage.TYPE_MESSAGE_PLAINTEXT);
} catch (RateLimitException e) {
Log.w("PushTransport", e);
throw new IOException("Rate limit exceeded.");
@@ -99,26 +107,26 @@ public class PushTransport extends BaseTransport {
return attachments;
}
private Pair<Integer, String> getEncryptedMessage(PushServiceSocket socket, Recipient recipient,
private Pair<Integer, byte[]> getEncryptedMessage(PushServiceSocket socket, Recipient recipient,
String canonicalRecipientNumber, String plaintext)
throws IOException
{
if (KeyUtil.isNonPrekeySessionFor(context, masterSecret, recipient)) {
Log.w("PushTransport", "Sending standard ciphertext message...");
String ciphertext = getEncryptedMessageForExistingSession(recipient, plaintext);
return new Pair<Integer, String>(OutgoingPushMessage.TYPE_MESSAGE_CIPHERTEXT, ciphertext);
byte[] ciphertext = getEncryptedMessageForExistingSession(recipient, plaintext);
return new Pair<Integer, byte[]>(OutgoingPushMessage.TYPE_MESSAGE_CIPHERTEXT, ciphertext);
} else if (KeyUtil.isSessionFor(context, recipient)) {
Log.w("PushTransport", "Sending prekeybundle ciphertext message for existing session...");
String ciphertext = getEncryptedPrekeyBundleMessageForExistingSession(recipient, plaintext);
return new Pair<Integer, String>(OutgoingPushMessage.TYPE_MESSAGE_PREKEY_BUNDLE, ciphertext);
byte[] ciphertext = getEncryptedPrekeyBundleMessageForExistingSession(recipient, plaintext);
return new Pair<Integer, byte[]>(OutgoingPushMessage.TYPE_MESSAGE_PREKEY_BUNDLE, ciphertext);
} else {
Log.w("PushTransport", "Sending prekeybundle ciphertext message for new session...");
String ciphertext = getEncryptedPrekeyBundleMessageForNewSession(socket, recipient, canonicalRecipientNumber, plaintext);
return new Pair<Integer, String>(OutgoingPushMessage.TYPE_MESSAGE_PREKEY_BUNDLE, ciphertext);
byte[] ciphertext = getEncryptedPrekeyBundleMessageForNewSession(socket, recipient, canonicalRecipientNumber, plaintext);
return new Pair<Integer, byte[]>(OutgoingPushMessage.TYPE_MESSAGE_PREKEY_BUNDLE, ciphertext);
}
}
private String getEncryptedPrekeyBundleMessageForExistingSession(Recipient recipient,
private byte[] getEncryptedPrekeyBundleMessageForExistingSession(Recipient recipient,
String plaintext)
{
IdentityKeyPair identityKeyPair = IdentityKeyUtil.getIdentityKeyPair(context, masterSecret);
@@ -131,7 +139,7 @@ public class PushTransport extends BaseTransport {
return preKeyBundleMessage.serialize();
}
private String getEncryptedPrekeyBundleMessageForNewSession(PushServiceSocket socket,
private byte[] getEncryptedPrekeyBundleMessageForNewSession(PushServiceSocket socket,
Recipient recipient,
String canonicalRecipientNumber,
String plaintext)
@@ -151,14 +159,14 @@ public class PushTransport extends BaseTransport {
return preKeyBundleMessage.serialize();
}
private String getEncryptedMessageForExistingSession(Recipient recipient, String plaintext)
private byte[] getEncryptedMessageForExistingSession(Recipient recipient, String plaintext)
throws IOException
{
IdentityKeyPair identityKeyPair = IdentityKeyUtil.getIdentityKeyPair(context, masterSecret);
MessageCipher messageCipher = new MessageCipher(context, masterSecret, identityKeyPair,
new PushTransportDetails());
return new String(messageCipher.encrypt(recipient, plaintext.getBytes()));
return messageCipher.encrypt(recipient, plaintext.getBytes());
}
}

View File

@@ -21,6 +21,7 @@ import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
import org.thoughtcrime.securesms.sms.SmsTransportDetails;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.textsecure.crypto.protocol.PreKeyBundleMessage;
import org.whispersystems.textsecure.util.Base64;
import java.util.ArrayList;
@@ -160,7 +161,7 @@ public class SmsTransport extends BaseTransport {
byte[] bundledMessage = messageCipher.encrypt(recipient, body.getBytes());
PreKeyBundleMessage preKeyBundleMessage = new PreKeyBundleMessage(identityKey.getPublicKey(), bundledMessage);
return new OutgoingPrekeyBundleMessage(message, preKeyBundleMessage.serialize());
return new OutgoingPrekeyBundleMessage(message, Base64.encodeBytesWithoutPadding(preKeyBundleMessage.serialize()));
}
}
}

View File

@@ -59,6 +59,10 @@ public class TextSecurePreferences {
setStringPreference(context, SIGNALING_KEY_PREF, signalingKey);
}
public static String getSignalingKey(Context context) {
return getStringPreference(context, SIGNALING_KEY_PREF, null);
}
public static boolean isEnterImeKeyEnabled(Context context) {
return getBooleanPreference(context, ENTER_PRESENT_PREF, false);
}