Verify identity keys on outgoing messages.

If PreKeyEntity identity key doesn't match local DB, fail
outgoing message and queue "incoming" identity key update
message for manual user approval.
This commit is contained in:
Moxie Marlinspike 2014-02-16 15:23:49 -08:00
parent e2989373cd
commit e7e5bc0884
16 changed files with 172 additions and 24 deletions

View File

@ -264,6 +264,7 @@
<string name="SmsMessageRecord_received_message_with_unknown_identity_key_click_to_process">
Received message with unknown identity key. Click to process and display.
</string>
<string name="SmsMessageRecord_received_updated_but_unknown_identity_information">Received updated but unknown identity information. Tap to validate identity.</string>
<!-- VerifyIdentityActivity -->

View File

@ -358,6 +358,7 @@ public class ConversationItem extends LinearLayout {
intent.putExtra("thread_id", messageRecord.getThreadId());
intent.putExtra("message_id", messageRecord.getId());
intent.putExtra("is_bundle", messageRecord.isBundleKeyExchange());
intent.putExtra("is_identity_update", messageRecord.isIdentityUpdate());
intent.putExtra("master_secret", masterSecret);
intent.putExtra("sent", messageRecord.isOutgoing());
context.startActivity(intent);

View File

@ -47,6 +47,7 @@ import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage;
import org.whispersystems.textsecure.crypto.protocol.PreKeyWhisperMessage;
import org.whispersystems.textsecure.storage.InvalidKeyIdException;
import org.whispersystems.textsecure.storage.RecipientDevice;
import org.whispersystems.textsecure.util.Base64;
import java.io.IOException;
@ -71,6 +72,7 @@ public class ReceiveKeyActivity extends Activity {
private MasterSecret masterSecret;
private PreKeyWhisperMessage keyExchangeMessageBundle;
private KeyExchangeMessage keyExchangeMessage;
private IdentityKey identityUpdateMessage;
@Override
protected void onCreate(Bundle state) {
@ -99,8 +101,11 @@ public class ReceiveKeyActivity extends Activity {
}
private void initializeText() {
if (isTrusted(keyExchangeMessage, keyExchangeMessageBundle)) initializeTrustedText();
else initializeUntrustedText();
if (isTrusted(keyExchangeMessage, keyExchangeMessageBundle, identityUpdateMessage)) {
initializeTrustedText();
} else {
initializeUntrustedText();
}
}
private void initializeTrustedText() {
@ -113,12 +118,16 @@ public class ReceiveKeyActivity extends Activity {
spannableString.setSpan(new ClickableSpan() {
@Override
public void onClick(View widget) {
IdentityKey remoteIdentity;
if (identityUpdateMessage != null) remoteIdentity = identityUpdateMessage;
else if (keyExchangeMessageBundle != null) remoteIdentity = keyExchangeMessageBundle.getIdentityKey();
else remoteIdentity = keyExchangeMessage.getIdentityKey();
Intent intent = new Intent(ReceiveKeyActivity.this, VerifyIdentityActivity.class);
intent.putExtra("recipient", recipient);
intent.putExtra("master_secret", masterSecret);
intent.putExtra("remote_identity",
keyExchangeMessage == null ?
keyExchangeMessageBundle.getIdentityKey() : keyExchangeMessage.getIdentityKey());
intent.putExtra("remote_identity", remoteIdentity);
startActivity(intent);
}
}, getString(R.string.ReceiveKeyActivity_the_signature_on_this_key_exchange_is_different).length() +1,
@ -128,7 +137,7 @@ public class ReceiveKeyActivity extends Activity {
descriptionText.setMovementMethod(LinkMovementMethod.getInstance());
}
private boolean isTrusted(KeyExchangeMessage message, PreKeyWhisperMessage messageBundle) {
private boolean isTrusted(KeyExchangeMessage message, PreKeyWhisperMessage messageBundle, IdentityKey identityUpdateMessage) {
RecipientDevice recipientDevice = new RecipientDevice(recipient.getRecipientId(), recipientDeviceId);
if (message != null) {
@ -138,6 +147,9 @@ public class ReceiveKeyActivity extends Activity {
} else if (messageBundle != null) {
KeyExchangeProcessorV2 processor = new KeyExchangeProcessorV2(this, masterSecret, recipientDevice);
return processor.isTrusted(messageBundle);
} else if (identityUpdateMessage != null) {
KeyExchangeProcessorV2 processor = new KeyExchangeProcessorV2(this, masterSecret, recipientDevice);
return processor.isTrusted(identityUpdateMessage);
}
return false;
@ -154,6 +166,8 @@ public class ReceiveKeyActivity extends Activity {
byte[] body = transportDetails.getDecodedMessage(messageBody.getBytes());
this.keyExchangeMessageBundle = new PreKeyWhisperMessage(body);
} else if (getIntent().getBooleanExtra("is_identity_update", false)) {
this.identityUpdateMessage = new IdentityKey(Base64.decodeWithoutPadding(messageBody), 0);
} else {
this.keyExchangeMessage = KeyExchangeMessage.createFor(messageBody);
}
@ -232,6 +246,11 @@ public class ReceiveKeyActivity extends Activity {
DatabaseFactory.getEncryptingSmsDatabase(ReceiveKeyActivity.this)
.markAsCorruptKeyExchange(messageId);
}
} else if (identityUpdateMessage != null) {
DatabaseFactory.getIdentityDatabase(ReceiveKeyActivity.this)
.saveIdentity(masterSecret, recipient.getRecipientId(), identityUpdateMessage);
DatabaseFactory.getSmsDatabase(ReceiveKeyActivity.this).markAsProcessedKeyExchange(messageId);
}

View File

@ -55,6 +55,10 @@ public class KeyExchangeProcessorV2 extends KeyExchangeProcessor {
return isTrusted(message.getIdentityKey());
}
public boolean isTrusted(PreKeyEntity entity) {
return isTrusted(entity.getIdentityKey());
}
public boolean isTrusted(KeyExchangeMessage message) {
return message.hasIdentityKey() && isTrusted(message.getIdentityKey());
}

View File

@ -35,6 +35,7 @@ public interface MmsSmsColumns {
protected static final long KEY_EXCHANGE_CORRUPTED_BIT = 0x1000;
protected static final long KEY_EXCHANGE_INVALID_VERSION_BIT = 0x800;
protected static final long KEY_EXCHANGE_BUNDLE_BIT = 0x400;
protected static final long KEY_EXCHANGE_IDENTITY_UPDATE_BIT = 0x200;
// Secure Message Information
protected static final long SECURE_MESSAGE_BIT = 0x800000;
@ -98,6 +99,10 @@ public interface MmsSmsColumns {
return (type & KEY_EXCHANGE_BUNDLE_BIT) != 0;
}
public static boolean isIdentityUpdate(long type) {
return (type & KEY_EXCHANGE_IDENTITY_UPDATE_BIT) != 0;
}
public static boolean isSymmetricEncryption(long type) {
return (type & ENCRYPTION_SYMMETRIC_BIT) != 0;
}

View File

@ -252,6 +252,7 @@ public class SmsDatabase extends Database implements MmsSmsColumns {
else if (((IncomingKeyExchangeMessage)message).isCorrupted()) type |= Types.KEY_EXCHANGE_CORRUPTED_BIT;
else if (((IncomingKeyExchangeMessage)message).isInvalidVersion()) type |= Types.KEY_EXCHANGE_INVALID_VERSION_BIT;
else if (((IncomingKeyExchangeMessage)message).isPreKeyBundle()) type |= Types.KEY_EXCHANGE_BUNDLE_BIT;
else if (((IncomingKeyExchangeMessage)message).isIdentityUpdate()) type |= Types.KEY_EXCHANGE_IDENTITY_UPDATE_BIT;
} else if (message.isSecureMessage()) {
type |= Types.SECURE_MESSAGE_BIT;
type |= Types.ENCRYPTION_REMOTE_BIT;

View File

@ -111,6 +111,10 @@ public abstract class MessageRecord extends DisplayRecord {
return SmsDatabase.Types.isBundleKeyExchange(type);
}
public boolean isIdentityUpdate() {
return SmsDatabase.Types.isIdentityUpdate(type);
}
public boolean isCorruptedKeyExchange() {
return SmsDatabase.Types.isCorruptedKeyExchange(type);
}

View File

@ -65,6 +65,8 @@ public class SmsMessageRecord extends MessageRecord {
return emphasisAdded(context.getString(R.string.SmsMessageRecord_received_key_exchange_message_for_invalid_protocol_version));
} else if (isBundleKeyExchange()) {
return emphasisAdded(context.getString(R.string.SmsMessageRecord_received_message_with_unknown_identity_key_click_to_process));
} else if (isIdentityUpdate()) {
return emphasisAdded(context.getString(R.string.SmsMessageRecord_received_updated_but_unknown_identity_information));
} else if (isKeyExchange() && isOutgoing()) {
return new SpannableString("");
} else if (isKeyExchange() && !isOutgoing()) {

View File

@ -30,10 +30,14 @@ import org.thoughtcrime.securesms.mms.MmsSendResult;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.service.SendReceiveService.ToastHandler;
import org.thoughtcrime.securesms.sms.IncomingIdentityUpdateMessage;
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
import org.thoughtcrime.securesms.transport.RetryLaterException;
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
import org.thoughtcrime.securesms.transport.UniversalTransport;
import org.thoughtcrime.securesms.transport.UntrustedIdentityException;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.util.Base64;
import ws.com.google.android.mms.MmsException;
import ws.com.google.android.mms.pdu.SendReq;
@ -87,6 +91,11 @@ public class MmsSender {
database.markAsSentFailed(message.getDatabaseMessageId());
Recipients recipients = threads.getRecipientsForThreadId(threadId);
MessageNotifier.notifyMessageDeliveryFailed(context, recipients, threadId);
} catch (UntrustedIdentityException uie) {
IncomingTextMessage base = new IncomingTextMessage(message);
IncomingIdentityUpdateMessage identityUpdateMessage = new IncomingIdentityUpdateMessage(base, Base64.encodeBytesWithoutPadding(uie.getIdentityKey().serialize()));
DatabaseFactory.getEncryptingSmsDatabase(context).insertMessageInbox(masterSecret, identityUpdateMessage);
database.markAsSentFailed(messageId);
} catch (RetryLaterException e) {
Log.w("MmsSender", e);
database.markAsOutbox(message.getDatabaseMessageId());

View File

@ -30,9 +30,13 @@ import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.service.SendReceiveService.ToastHandler;
import org.thoughtcrime.securesms.sms.IncomingIdentityUpdateMessage;
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
import org.thoughtcrime.securesms.transport.UniversalTransport;
import org.thoughtcrime.securesms.transport.UntrustedIdentityException;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.util.Base64;
public class SmsSender {
@ -71,12 +75,21 @@ public class SmsSender {
else reader = database.getOutgoingMessages(masterSecret);
while (reader != null && (record = reader.getNext()) != null) {
try {
database.markAsSending(record.getId());
transport.deliver(record);
}
} catch (UntrustedIdentityException e) {
Log.w("SmsSender", e);
IncomingTextMessage base = new IncomingTextMessage(record);
IncomingIdentityUpdateMessage identityUpdateMessage = new IncomingIdentityUpdateMessage(base, Base64.encodeBytesWithoutPadding(e.getIdentityKey().serialize()));
DatabaseFactory.getEncryptingSmsDatabase(context).insertMessageInbox(masterSecret, identityUpdateMessage);
DatabaseFactory.getSmsDatabase(context).markAsSentFailed(messageId);
} catch (UndeliverableMessageException ude) {
Log.w("SmsSender", ude);
DatabaseFactory.getSmsDatabase(context).markAsSentFailed(messageId);
}
}
} finally {
if (reader != null)
reader.close();

View File

@ -0,0 +1,18 @@
package org.thoughtcrime.securesms.sms;
public class IncomingIdentityUpdateMessage extends IncomingKeyExchangeMessage {
public IncomingIdentityUpdateMessage(IncomingTextMessage base, String newBody) {
super(base, newBody);
}
@Override
public IncomingIdentityUpdateMessage withMessageBody(String messageBody) {
return new IncomingIdentityUpdateMessage(this, messageBody);
}
@Override
public boolean isIdentityUpdate() {
return true;
}
}

View File

@ -1,7 +1,5 @@
package org.thoughtcrime.securesms.sms;
import org.whispersystems.textsecure.push.IncomingPushMessage;
public class IncomingPreKeyBundleMessage extends IncomingKeyExchangeMessage {
public IncomingPreKeyBundleMessage(IncomingTextMessage base, String newBody) {

View File

@ -4,12 +4,15 @@ import android.os.Parcel;
import android.os.Parcelable;
import android.telephony.SmsMessage;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.whispersystems.textsecure.push.IncomingPushMessage;
import org.whispersystems.textsecure.storage.RecipientDevice;
import java.util.List;
import ws.com.google.android.mms.pdu.SendReq;
import static org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent.GroupContext;
public class IncomingTextMessage implements Parcelable {
@ -121,6 +124,34 @@ public class IncomingTextMessage implements Parcelable {
this.groupActionArgument = fragments.get(0).getGroupActionArgument();
}
public IncomingTextMessage(SendReq record) {
this.message = "";
this.sender = record.getTo()[0].getString();
this.senderDeviceId = RecipientDevice.DEFAULT_DEVICE_ID;
this.protocol = 31338;
this.serviceCenterAddress = "Outgoing";
this.replyPathPresent = true;
this.pseudoSubject = "";
this.sentTimestampMillis = System.currentTimeMillis();
this.groupId = null;
this.groupAction = -1;
this.groupActionArgument = null;
}
public IncomingTextMessage(SmsMessageRecord record) {
this.message = record.getBody().getBody();
this.sender = record.getIndividualRecipient().getNumber();
this.senderDeviceId = RecipientDevice.DEFAULT_DEVICE_ID;
this.protocol = 31338;
this.serviceCenterAddress = "Outgoing";
this.replyPathPresent = true;
this.pseudoSubject = "";
this.sentTimestampMillis = System.currentTimeMillis();
this.groupId = null;
this.groupAction = -1;
this.groupActionArgument = null;
}
public long getSentTimestampMillis() {
return sentTimestampMillis;
}
@ -169,6 +200,10 @@ public class IncomingTextMessage implements Parcelable {
return false;
}
public boolean isIdentityUpdate() {
return false;
}
public String getGroupId() {
return groupId;
}

View File

@ -73,7 +73,9 @@ public class PushTransport extends BaseTransport {
this.masterSecret = masterSecret;
}
public void deliver(SmsMessageRecord message) throws IOException {
public void deliver(SmsMessageRecord message)
throws IOException, UntrustedIdentityException
{
try {
Recipient recipient = message.getIndividualRecipient();
long threadId = message.getThreadId();
@ -97,7 +99,9 @@ public class PushTransport extends BaseTransport {
}
}
public void deliver(SendReq message, long threadId) throws IOException {
public void deliver(SendReq message, long threadId)
throws IOException, UntrustedIdentityException
{
PushServiceSocket socket = PushServiceSocketFactory.create(context);
byte[] plaintext = getPlaintextMessage(socket, message);
String destination = message.getTo()[0].getString();
@ -147,6 +151,9 @@ public class PushTransport extends BaseTransport {
} catch (IOException e) {
Log.w("PushTransport", e);
failures.add(recipient);
} catch (UntrustedIdentityException e) {
Log.w("PushTransport", e);
failures.add(recipient);
}
}
@ -165,7 +172,7 @@ public class PushTransport extends BaseTransport {
}
private void deliver(PushServiceSocket socket, Recipient recipient, long threadId, byte[] plaintext)
throws IOException, InvalidNumberException
throws IOException, InvalidNumberException, UntrustedIdentityException
{
for (int i=0;i<3;i++) {
try {
@ -274,7 +281,7 @@ public class PushTransport extends BaseTransport {
private OutgoingPushMessageList getEncryptedMessages(PushServiceSocket socket, long threadId,
Recipient recipient, byte[] plaintext)
throws IOException, InvalidNumberException
throws IOException, InvalidNumberException, UntrustedIdentityException
{
String e164number = Util.canonicalizeNumber(context, recipient.getNumber());
long recipientId = recipient.getRecipientId();
@ -296,7 +303,7 @@ public class PushTransport extends BaseTransport {
private PushBody getEncryptedMessage(PushServiceSocket socket, long threadId,
PushAddress pushAddress, byte[] plaintext)
throws IOException
throws IOException, UntrustedIdentityException
{
if (!SessionRecordV2.hasSession(context, masterSecret, pushAddress)) {
try {
@ -306,7 +313,11 @@ public class PushTransport extends BaseTransport {
PushAddress device = PushAddress.create(context, pushAddress.getRecipientId(), pushAddress.getNumber(), preKey.getDeviceId());
KeyExchangeProcessorV2 processor = new KeyExchangeProcessorV2(context, masterSecret, device);
if (processor.isTrusted(preKey)) {
processor.processKeyExchangeMessage(preKey, threadId);
} else {
throw new UntrustedIdentityException("Untrusted identity key!", preKey.getIdentityKey());
}
}
} catch (InvalidKeyException e) {
throw new IOException(e);

View File

@ -18,7 +18,6 @@ package org.thoughtcrime.securesms.transport;
import android.content.Context;
import android.util.Log;
import android.widget.Toast;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.mms.MmsSendResult;
@ -54,7 +53,9 @@ public class UniversalTransport {
this.mmsTransport = new MmsTransport(context, masterSecret);
}
public void deliver(SmsMessageRecord message) throws UndeliverableMessageException {
public void deliver(SmsMessageRecord message)
throws UndeliverableMessageException, UntrustedIdentityException
{
if (!TextSecurePreferences.isPushRegistered(context)) {
smsTransport.deliver(message);
return;
@ -83,7 +84,7 @@ public class UniversalTransport {
}
public MmsSendResult deliver(SendReq mediaMessage, long threadId)
throws UndeliverableMessageException, RetryLaterException
throws UndeliverableMessageException, RetryLaterException, UntrustedIdentityException
{
if (Util.isEmpty(mediaMessage.getTo())) {
throw new UndeliverableMessageException("No destination specified");
@ -97,14 +98,23 @@ public class UniversalTransport {
return mmsTransport.deliver(mediaMessage);
}
if (isPushTransport(mediaMessage.getTo()[0].getString())) {
String destination;
try {
destination = Util.canonicalizeNumber(context, mediaMessage.getTo()[0].getString());
} catch (InvalidNumberException ine) {
Log.w("UniversalTransport", ine);
return mmsTransport.deliver(mediaMessage);
}
if (isPushTransport(destination)) {
try {
Log.w("UniversalTransport", "Delivering media message with GCM...");
pushTransport.deliver(mediaMessage, threadId);
return new MmsSendResult("push".getBytes("UTF-8"), 0, true);
} catch (IOException ioe) {
Log.w("UniversalTransport", ioe);
if (!GroupUtil.isEncodedGroup(mediaMessage.getTo()[0].getString())) {
if (!GroupUtil.isEncodedGroup(destination)) {
return mmsTransport.deliver(mediaMessage);
} else {
throw new RetryLaterException();

View File

@ -0,0 +1,17 @@
package org.thoughtcrime.securesms.transport;
import org.whispersystems.textsecure.crypto.IdentityKey;
public class UntrustedIdentityException extends Exception {
private final IdentityKey identityKey;
public UntrustedIdentityException(String s, IdentityKey identityKey) {
super(s);
this.identityKey = identityKey;
}
public IdentityKey getIdentityKey() {
return identityKey;
}
}