Added SMS transport support for PreKeyBundle messages.

1) Added SMS transport support.

2) Keep track of whether a PreKeyBundle message has gotten
   a response, and send them as subsequent messages until
   one has been received.
This commit is contained in:
Moxie Marlinspike 2013-08-21 19:34:11 -07:00
parent c3b8b62d32
commit 1bbcedabd4
14 changed files with 221 additions and 42 deletions

View File

@ -107,6 +107,11 @@ public class KeyUtil {
(SessionRecord.hasSession(context, recipient));
}
public static boolean isNonPrekeySessionFor(Context context, MasterSecret masterSecret, CanonicalRecipientAddress recipient) {
return isSessionFor(context, recipient) &&
!(new SessionRecord(context, masterSecret, recipient).isPrekeyBundleRequired());
}
public static boolean isIdentityKeyFor(Context context,
MasterSecret masterSecret,
CanonicalRecipientAddress recipient)

View File

@ -36,17 +36,17 @@ public class MessageCipher {
public static final int SUPPORTED_VERSION = 2;
public static final int CRADLE_AGREEMENT_VERSION = 2;
static final int VERSION_LENGTH = 1;
public static final int VERSION_LENGTH = 1;
private static final int SENDER_KEY_ID_LENGTH = 3;
private static final int RECEIVER_KEY_ID_LENGTH = 3;
static final int NEXT_KEY_LENGTH = PublicKey.KEY_SIZE;
public static final int NEXT_KEY_LENGTH = PublicKey.KEY_SIZE;
private static final int COUNTER_LENGTH = 3;
public static final int HEADER_LENGTH = VERSION_LENGTH + SENDER_KEY_ID_LENGTH + RECEIVER_KEY_ID_LENGTH + COUNTER_LENGTH + NEXT_KEY_LENGTH;
static final int VERSION_OFFSET = 0;
public static final int VERSION_OFFSET = 0;
private static final int SENDER_KEY_ID_OFFSET = VERSION_OFFSET + VERSION_LENGTH;
static final int RECEIVER_KEY_ID_OFFSET = SENDER_KEY_ID_OFFSET + SENDER_KEY_ID_LENGTH;
static final int NEXT_KEY_OFFSET = RECEIVER_KEY_ID_OFFSET + RECEIVER_KEY_ID_LENGTH;
public static final int RECEIVER_KEY_ID_OFFSET = SENDER_KEY_ID_OFFSET + SENDER_KEY_ID_LENGTH;
public static final int NEXT_KEY_OFFSET = RECEIVER_KEY_ID_OFFSET + RECEIVER_KEY_ID_LENGTH;
private static final int COUNTER_OFFSET = NEXT_KEY_OFFSET + NEXT_KEY_LENGTH;
private static final int TEXT_OFFSET = COUNTER_OFFSET + COUNTER_LENGTH;

View File

@ -131,6 +131,7 @@ public class SessionCipher {
context.getSessionRecord().setSessionKey(context.getSessionKey());
context.getSessionRecord().setSessionVersion(context.getNegotiatedVersion());
context.getSessionRecord().setPrekeyBundleRequired(false);
context.getSessionRecord().save();
return plaintextWithPadding;

View File

@ -0,0 +1,44 @@
/**
* 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 <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.push;
import org.whispersystems.textsecure.crypto.TransportDetails;
import org.whispersystems.textsecure.util.Base64;
import java.io.IOException;
public class PushTransportDetails implements TransportDetails {
@Override
public byte[] getStrippedPaddingMessageBody(byte[] messageWithPadding) {
return messageWithPadding;
}
@Override
public byte[] getPaddedMessageBody(byte[] messageBody) {
return messageBody;
}
@Override
public byte[] getEncodedMessage(byte[] messageWithMac) {
return Base64.encodeBytesWithoutPadding(messageWithMac).getBytes();
}
@Override
public byte[] getDecodedMessage(byte[] encodedMessageBytes) throws IOException {
return Base64.decodeWithoutPadding(new String(encodedMessageBytes));
}
}

View File

@ -36,8 +36,9 @@ import java.nio.channels.FileChannel;
*/
public class SessionRecord extends Record {
private static final int CURRENT_VERSION_MARKER = 0X55555556;
private static final int[] VALID_VERSION_MARKERS = {CURRENT_VERSION_MARKER, 0X55555555};
private static final int CURRENT_VERSION_MARKER = 0X55555557;
private static final int[] VALID_VERSION_MARKERS = {CURRENT_VERSION_MARKER, 0X55555556, 0X55555555};
private static final Object FILE_LOCK = new Object();
private int counter;
@ -48,6 +49,7 @@ public class SessionRecord extends Record {
private IdentityKey identityKey;
private SessionKey sessionKeyRecord;
private boolean verifiedSessionKey;
private boolean prekeyBundleRequired;
private final MasterSecret masterSecret;
@ -63,7 +65,7 @@ public class SessionRecord extends Record {
}
public static void delete(Context context, CanonicalRecipientAddress recipient) {
delete(context, SESSIONS_DIRECTORY, getRecipientId(context, recipient)+"");
delete(context, SESSIONS_DIRECTORY, getRecipientId(context, recipient) + "");
}
public static boolean hasSession(Context context, CanonicalRecipientAddress recipient) {
@ -116,6 +118,14 @@ public class SessionRecord extends Record {
return this.identityKey;
}
public boolean isPrekeyBundleRequired() {
return prekeyBundleRequired;
}
public void setPrekeyBundleRequired(boolean prekeyBundleRequired) {
this.prekeyBundleRequired = prekeyBundleRequired;
}
// public void setVerifiedSessionKey(boolean verifiedSessionKey) {
// this.verifiedSessionKey = verifiedSessionKey;
// }
@ -162,6 +172,7 @@ public class SessionRecord extends Record {
writeInteger(sessionVersion, out);
writeIdentityKey(out);
writeInteger(verifiedSessionKey ? 1 : 0, out);
writeInteger(prekeyBundleRequired ? 1 : 0, out);
if (sessionKeyRecord != null)
writeBlob(sessionKeyRecord.serialize(), out);
@ -202,6 +213,10 @@ public class SessionRecord extends Record {
this.verifiedSessionKey = (readInteger(in) == 1);
}
if (versionMarker >= 0X55555557) {
this.prekeyBundleRequired = (readInteger(in) == 1);
}
if (in.available() != 0)
this.sessionKeyRecord = new SessionKey(readBlob(in), masterSecret);
@ -226,4 +241,5 @@ public class SessionRecord extends Record {
return null;
}
}

View File

@ -110,6 +110,11 @@ public class KeyExchangeProcessor {
Log.w("KeyExchangeProcessor", "Received pre-key with remote key ID: " + remoteKey.getId());
Log.w("KeyExchangeProcessor", "Received pre-key with local key ID: " + preKeyId);
if (!PreKeyRecord.hasRecord(context, preKeyId) && KeyUtil.isSessionFor(context, recipient)) {
Log.w("KeyExchangeProcessor", "We've already processed the prekey part, letting bundled message fall through...");
return;
}
if (!PreKeyRecord.hasRecord(context, preKeyId))
throw new InvalidKeyIdException("No such prekey: " + preKeyId);
@ -152,6 +157,7 @@ public class KeyExchangeProcessor {
remoteKeyRecord.getCurrentRemoteKey().getFingerprintBytes());
sessionRecord.setIdentityKey(message.getIdentityKey());
sessionRecord.setSessionVersion(MessageCipher.SUPPORTED_VERSION);
sessionRecord.setPrekeyBundleRequired(true);
sessionRecord.save();
DatabaseFactory.getIdentityDatabase(context)

View File

@ -0,0 +1,8 @@
package org.thoughtcrime.securesms.protocol;
public class PrekeyBundleWirePrefix extends WirePrefix {
@Override
public String calculatePrefix(String message) {
return super.calculatePreKeyBundlePrefix(message);
}
}

View File

@ -56,6 +56,8 @@ public class MultipartSmsMessageHandler {
if (message.getWireType() == MultipartSmsTransportMessage.WIRETYPE_KEY) {
return new IncomingKeyExchangeMessage(message.getBaseMessage(), strippedMessage);
} else if (message.getWireType() == MultipartSmsTransportMessage.WIRETYPE_PREKEY) {
return new IncomingPreKeyBundleMessage(message.getBaseMessage(), strippedMessage);
} else {
return new IncomingEncryptedMessage(message.getBaseMessage(), strippedMessage);
}
@ -67,6 +69,8 @@ public class MultipartSmsMessageHandler {
if (message.getWireType() == MultipartSmsTransportMessage.WIRETYPE_KEY) {
return new IncomingKeyExchangeMessage(message.getBaseMessage(), strippedMessage);
} else if (message.getWireType() == MultipartSmsTransportMessage.WIRETYPE_PREKEY) {
return new IncomingPreKeyBundleMessage(message.getBaseMessage(), strippedMessage);
} else {
return new IncomingEncryptedMessage(message.getBaseMessage(), strippedMessage);
}

View File

@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.sms;
import android.util.Log;
import org.thoughtcrime.securesms.protocol.KeyExchangeWirePrefix;
import org.thoughtcrime.securesms.protocol.PrekeyBundleWirePrefix;
import org.thoughtcrime.securesms.protocol.SecureMessageWirePrefix;
import org.thoughtcrime.securesms.protocol.WirePrefix;
import org.whispersystems.textsecure.util.Base64;
@ -22,6 +23,7 @@ public class MultipartSmsTransportMessage {
public static final int WIRETYPE_SECURE = 1;
public static final int WIRETYPE_KEY = 2;
public static final int WIRETYPE_PREKEY = 3;
private static final int VERSION_OFFSET = 0;
private static final int MULTIPART_OFFSET = 1;
@ -33,9 +35,12 @@ public class MultipartSmsTransportMessage {
public MultipartSmsTransportMessage(IncomingTextMessage message) throws IOException {
this.message = message;
this.wireType = WirePrefix.isEncryptedMessage(message.getMessageBody()) ? WIRETYPE_SECURE : WIRETYPE_KEY;
this.decodedMessage = Base64.decodeWithoutPadding(message.getMessageBody().substring(WirePrefix.PREFIX_SIZE));
if (WirePrefix.isEncryptedMessage(message.getMessageBody())) wireType = WIRETYPE_SECURE;
else if (WirePrefix.isPreKeyBundle(message.getMessageBody())) wireType = WIRETYPE_PREKEY;
else wireType = WIRETYPE_KEY;
Log.w(TAG, "Decoded message with version: " + getCurrentVersion());
}
@ -151,8 +156,9 @@ public class MultipartSmsTransportMessage {
WirePrefix prefix;
if (message.isKeyExchange()) prefix = new KeyExchangeWirePrefix();
else prefix = new SecureMessageWirePrefix();
if (message.isKeyExchange()) prefix = new KeyExchangeWirePrefix();
else if (message.isPreKeyBundle()) prefix = new PrekeyBundleWirePrefix();
else prefix = new SecureMessageWirePrefix();
if (count == 1) return getSingleEncoded(decoded, prefix);
else return getMultiEncoded(decoded, prefix, count, identifier);

View File

@ -0,0 +1,19 @@
package org.thoughtcrime.securesms.sms;
public class OutgoingPrekeyBundleMessage extends OutgoingTextMessage {
public OutgoingPrekeyBundleMessage(OutgoingTextMessage message, String body) {
super(message, body);
}
@Override
public boolean isPreKeyBundle() {
return true;
}
@Override
public OutgoingTextMessage withBody(String body) {
return new OutgoingPrekeyBundleMessage(this, body);
}
}

View File

@ -19,8 +19,8 @@ public class OutgoingTextMessage {
}
protected OutgoingTextMessage(OutgoingTextMessage base, String body) {
this.recipients = base.getRecipients();
this.message = body;
this.recipients = base.getRecipients();
this.message = body;
}
public String getMessageBody() {
@ -39,6 +39,10 @@ public class OutgoingTextMessage {
return false;
}
public boolean isPreKeyBundle() {
return false;
}
public static OutgoingTextMessage from(SmsMessageRecord record) {
if (record.isSecure()) {
return new OutgoingEncryptedMessage(record.getIndividualRecipient(), record.getBody().getBody());

View File

@ -2,26 +2,27 @@ package org.thoughtcrime.securesms.transport;
import android.content.Context;
import android.util.Log;
import android.util.Pair;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.crypto.KeyExchangeProcessor;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.mms.PartParser;
import org.thoughtcrime.securesms.mms.TextTransport;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.sms.RawTransportDetails;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.textsecure.crypto.IdentityKey;
import org.whispersystems.textsecure.crypto.IdentityKeyPair;
import org.whispersystems.textsecure.crypto.KeyUtil;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.crypto.MessageCipher;
import org.whispersystems.textsecure.crypto.protocol.PreKeyBundleMessage;
import org.whispersystems.textsecure.push.PreKeyEntity;
import org.whispersystems.textsecure.push.PushAttachmentData;
import org.whispersystems.textsecure.push.PushServiceSocket;
import org.whispersystems.textsecure.push.PushTransportDetails;
import org.whispersystems.textsecure.push.RateLimitException;
import org.whispersystems.textsecure.storage.SessionRecord;
import org.whispersystems.textsecure.util.PhoneNumberFormatter;
import java.io.IOException;
@ -53,20 +54,14 @@ public class PushTransport extends BaseTransport {
String password = TextSecurePreferences.getPushServerPassword(context);
PushServiceSocket socket = new PushServiceSocket(context, localNumber, password);
Recipient recipient = message.getIndividualRecipient();
String plaintext = message.getBody().getBody();
String recipientCanonicalNumber = PhoneNumberFormatter.formatNumber(recipient.getNumber(),
localNumber);
Recipient recipient = message.getIndividualRecipient();
String plaintext = message.getBody().getBody();
String recipientCanonicalNumber = PhoneNumberFormatter.formatNumber(recipient.getNumber(),
localNumber);
if (SessionRecord.hasSession(context, recipient)) {
byte[] cipherText = getEncryptedMessageForExistingSession(recipient, plaintext);
socket.sendMessage(recipientCanonicalNumber, new String(cipherText), TYPE_MESSAGE_CIPHERTEXT);
} else {
byte[] cipherText = getEncryptedMessageForNewSession(socket, recipient,
recipientCanonicalNumber,
plaintext);
socket.sendMessage(recipientCanonicalNumber, new String(cipherText), TYPE_MESSAGE_PREKEY_BUNDLE);
}
Pair<Integer, String> typeAndCiphertext = getEncryptedMessage(socket, recipient, recipientCanonicalNumber, plaintext);
socket.sendMessage(recipientCanonicalNumber, typeAndCiphertext.second, typeAndCiphertext.first);
context.sendBroadcast(constructSentIntent(context, message.getId(), message.getType()));
} catch (RateLimitException e) {
@ -108,8 +103,42 @@ public class PushTransport extends BaseTransport {
return attachments;
}
private byte[] getEncryptedMessageForNewSession(PushServiceSocket socket, Recipient recipient,
String canonicalRecipientNumber, String plaintext)
private Pair<Integer, String> 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>(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>(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>(TYPE_MESSAGE_PREKEY_BUNDLE, ciphertext);
}
}
private String getEncryptedPrekeyBundleMessageForExistingSession(Recipient recipient,
String plaintext)
{
IdentityKeyPair identityKeyPair = IdentityKeyUtil.getIdentityKeyPair(context, masterSecret);
IdentityKey identityKey = identityKeyPair.getPublicKey();
MessageCipher message = new MessageCipher(context, masterSecret, identityKeyPair, new RawTransportDetails());
byte[] bundledMessage = message.encrypt(recipient, plaintext.getBytes());
PreKeyBundleMessage preKeyBundleMessage = new PreKeyBundleMessage(identityKey, bundledMessage);
return preKeyBundleMessage.serialize();
}
private String getEncryptedPrekeyBundleMessageForNewSession(PushServiceSocket socket,
Recipient recipient,
String canonicalRecipientNumber,
String plaintext)
throws IOException
{
IdentityKeyPair identityKeyPair = IdentityKeyUtil.getIdentityKeyPair(context, masterSecret);
@ -123,15 +152,17 @@ public class PushTransport extends BaseTransport {
byte[] bundledMessage = message.encrypt(recipient, plaintext.getBytes());
PreKeyBundleMessage preKeyBundleMessage = new PreKeyBundleMessage(identityKey, bundledMessage);
return preKeyBundleMessage.serialize().getBytes();
return preKeyBundleMessage.serialize();
}
private byte[] getEncryptedMessageForExistingSession(Recipient recipient, String plaintext)
private String getEncryptedMessageForExistingSession(Recipient recipient, String plaintext)
throws IOException
{
IdentityKeyPair identityKeyPair = IdentityKeyUtil.getIdentityKeyPair(context, masterSecret);
MessageCipher message = new MessageCipher(context, masterSecret, identityKeyPair, new TextTransport());
return message.encrypt(recipient, plaintext.getBytes());
IdentityKeyPair identityKeyPair = IdentityKeyUtil.getIdentityKeyPair(context, masterSecret);
MessageCipher messageCipher = new MessageCipher(context, masterSecret, identityKeyPair,
new PushTransportDetails());
return new String(messageCipher.encrypt(recipient, plaintext.getBytes()));
}
}

View File

@ -4,9 +4,12 @@ import android.app.PendingIntent;
import android.content.Context;
import android.telephony.SmsManager;
import android.util.Log;
import android.util.Pair;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.sms.OutgoingPrekeyBundleMessage;import org.thoughtcrime.securesms.sms.RawTransportDetails;
import org.whispersystems.textsecure.crypto.IdentityKeyPair;
import org.whispersystems.textsecure.crypto.KeyUtil;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.crypto.MessageCipher;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
@ -17,6 +20,7 @@ import org.thoughtcrime.securesms.sms.MultipartSmsMessageHandler;
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 java.util.ArrayList;
@ -43,9 +47,7 @@ public class SmsTransport extends BaseTransport {
OutgoingTextMessage transportMessage = OutgoingTextMessage.from(message);
if (message.isSecure()) {
String encryptedMessage = getAsymmetricEncrypt(masterSecret, message.getBody().getBody(),
message.getIndividualRecipient());
transportMessage = transportMessage.withBody(encryptedMessage);
transportMessage = getAsymmetricEncrypt(masterSecret, transportMessage);
}
ArrayList<String> messages = multipartMessageHandler.divideMessage(transportMessage);
@ -139,9 +141,26 @@ public class SmsTransport extends BaseTransport {
return deliveredIntents;
}
private String getAsymmetricEncrypt(MasterSecret masterSecret, String body, Recipient recipient) {
IdentityKeyPair identityKey = IdentityKeyUtil.getIdentityKeyPair(context, masterSecret);
MessageCipher message = new MessageCipher(context, masterSecret, identityKey, new SmsTransportDetails());
return new String(message.encrypt(recipient, body.getBytes()));
private OutgoingTextMessage getAsymmetricEncrypt(MasterSecret masterSecret,
OutgoingTextMessage message)
{
Recipient recipient = message.getRecipients().getPrimaryRecipient();
String body = message.getMessageBody();
IdentityKeyPair identityKey = IdentityKeyUtil.getIdentityKeyPair(context, masterSecret);
if (KeyUtil.isNonPrekeySessionFor(context, masterSecret, recipient)) {
Log.w("SmsTransport", "Delivering standard ciphertext...");
MessageCipher messageCipher = new MessageCipher(context, masterSecret, identityKey, new SmsTransportDetails());
byte[] ciphertext = messageCipher.encrypt(recipient, body.getBytes());
return message.withBody(new String(ciphertext));
} else {
Log.w("SmsTransport", "Delivering prekeybundle ciphertext...");
MessageCipher messageCipher = new MessageCipher(context, masterSecret, identityKey, new RawTransportDetails());
byte[] bundledMessage = messageCipher.encrypt(recipient, body.getBytes());
PreKeyBundleMessage preKeyBundleMessage = new PreKeyBundleMessage(identityKey.getPublicKey(), bundledMessage);
return new OutgoingPrekeyBundleMessage(message, preKeyBundleMessage.serialize());
}
}
}

View File

@ -1,3 +1,19 @@
/**
* 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 <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.transport;
import android.content.Context;