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.
fork-5.53.8
Moxie Marlinspike 2014-02-16 15:23:49 -08:00
rodzic e2989373cd
commit e7e5bc0884
16 zmienionych plików z 172 dodań i 24 usunięć

Wyświetl plik

@ -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 -->

Wyświetl plik

@ -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);

Wyświetl plik

@ -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);
}

Wyświetl plik

@ -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());
}

Wyświetl plik

@ -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;
}

Wyświetl plik

@ -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;

Wyświetl plik

@ -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);
}

Wyświetl plik

@ -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()) {

Wyświetl plik

@ -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());

Wyświetl plik

@ -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) {
database.markAsSending(record.getId());
transport.deliver(record);
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);
}
}
} catch (UndeliverableMessageException ude) {
Log.w("SmsSender", ude);
DatabaseFactory.getSmsDatabase(context).markAsSentFailed(messageId);
} finally {
if (reader != null)
reader.close();

Wyświetl plik

@ -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;
}
}

Wyświetl plik

@ -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) {

Wyświetl plik

@ -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;
}

Wyświetl plik

@ -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);
processor.processKeyExchangeMessage(preKey, threadId);
if (processor.isTrusted(preKey)) {
processor.processKeyExchangeMessage(preKey, threadId);
} else {
throw new UntrustedIdentityException("Untrusted identity key!", preKey.getIdentityKey());
}
}
} catch (InvalidKeyException e) {
throw new IOException(e);

Wyświetl plik

@ -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();

Wyświetl plik

@ -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;
}
}