Remove encrypted SMS transport, simplify transport options.

Closes #2647

// FREEBIE
This commit is contained in:
Moxie Marlinspike
2015-03-11 14:23:45 -07:00
parent 2011391e65
commit a4e18c515c
57 changed files with 541 additions and 1952 deletions

View File

@@ -1,89 +0,0 @@
/**
* Copyright (C) 2011 Whisper Systems
* 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.crypto;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore;
import org.thoughtcrime.securesms.crypto.storage.TextSecurePreKeyStore;
import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.sms.OutgoingKeyExchangeMessage;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.Dialogs;
import org.whispersystems.libaxolotl.AxolotlAddress;
import org.whispersystems.libaxolotl.SessionBuilder;
import org.whispersystems.libaxolotl.protocol.KeyExchangeMessage;
import org.whispersystems.libaxolotl.state.IdentityKeyStore;
import org.whispersystems.libaxolotl.state.PreKeyStore;
import org.whispersystems.libaxolotl.state.SessionRecord;
import org.whispersystems.libaxolotl.state.SessionStore;
import org.whispersystems.libaxolotl.state.SignedPreKeyStore;
import org.whispersystems.textsecure.api.push.TextSecureAddress;
public class KeyExchangeInitiator {
public static void initiate(final Context context, final MasterSecret masterSecret, final Recipient recipient, boolean promptOnExisting) {
if (promptOnExisting && hasInitiatedSession(context, masterSecret, recipient)) {
AlertDialog.Builder dialog = new AlertDialog.Builder(context);
dialog.setTitle(R.string.KeyExchangeInitiator_initiate_despite_existing_request_question);
dialog.setMessage(R.string.KeyExchangeInitiator_youve_already_sent_a_session_initiation_request_to_this_recipient_are_you_sure);
dialog.setIcon(Dialogs.resolveIcon(context, R.attr.dialog_alert_icon));
dialog.setCancelable(true);
dialog.setPositiveButton(R.string.KeyExchangeInitiator_send, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
initiateKeyExchange(context, masterSecret, recipient);
}
});
dialog.setNegativeButton(android.R.string.cancel, null);
dialog.show();
} else {
initiateKeyExchange(context, masterSecret, recipient);
}
}
private static void initiateKeyExchange(Context context, MasterSecret masterSecret, Recipient recipient) {
SessionStore sessionStore = new TextSecureSessionStore(context, masterSecret);
PreKeyStore preKeyStore = new TextSecurePreKeyStore(context, masterSecret);
SignedPreKeyStore signedPreKeyStore = new TextSecurePreKeyStore(context, masterSecret);
IdentityKeyStore identityKeyStore = new TextSecureIdentityKeyStore(context, masterSecret);
SessionBuilder sessionBuilder = new SessionBuilder(sessionStore, preKeyStore, signedPreKeyStore,
identityKeyStore, new AxolotlAddress(recipient.getNumber(),
TextSecureAddress.DEFAULT_DEVICE_ID));
KeyExchangeMessage keyExchangeMessage = sessionBuilder.process();
String serializedMessage = Base64.encodeBytesWithoutPadding(keyExchangeMessage.serialize());
OutgoingKeyExchangeMessage textMessage = new OutgoingKeyExchangeMessage(recipient, serializedMessage);
MessageSender.send(context, masterSecret, textMessage, -1, false);
}
private static boolean hasInitiatedSession(Context context, MasterSecret masterSecret,
Recipient recipient)
{
SessionStore sessionStore = new TextSecureSessionStore(context, masterSecret);
SessionRecord sessionRecord = sessionStore.loadSession(new AxolotlAddress(recipient.getNumber(), TextSecureAddress.DEFAULT_DEVICE_ID));
return sessionRecord.getSessionState().hasPendingKeyExchange();
}
}

View File

@@ -1,134 +0,0 @@
package org.thoughtcrime.securesms.crypto;
import android.content.Context;
import android.util.Log;
import org.thoughtcrime.securesms.mms.TextTransport;
import org.thoughtcrime.securesms.protocol.WirePrefix;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libaxolotl.AxolotlAddress;
import org.whispersystems.libaxolotl.DuplicateMessageException;
import org.whispersystems.libaxolotl.InvalidMessageException;
import org.whispersystems.libaxolotl.LegacyMessageException;
import org.whispersystems.libaxolotl.NoSessionException;
import org.whispersystems.libaxolotl.SessionCipher;
import org.whispersystems.libaxolotl.protocol.CiphertextMessage;
import org.whispersystems.libaxolotl.protocol.WhisperMessage;
import org.whispersystems.libaxolotl.state.AxolotlStore;
import org.whispersystems.libaxolotl.util.guava.Optional;
import org.whispersystems.textsecure.api.push.TextSecureAddress;
import java.io.IOException;
import ws.com.google.android.mms.ContentType;
import ws.com.google.android.mms.pdu.EncodedStringValue;
import ws.com.google.android.mms.pdu.MultimediaMessagePdu;
import ws.com.google.android.mms.pdu.PduBody;
import ws.com.google.android.mms.pdu.PduComposer;
import ws.com.google.android.mms.pdu.PduParser;
import ws.com.google.android.mms.pdu.PduPart;
import ws.com.google.android.mms.pdu.RetrieveConf;
import ws.com.google.android.mms.pdu.SendReq;
public class MmsCipher {
private static final String TAG = MmsCipher.class.getSimpleName();
private final TextTransport textTransport = new TextTransport();
private final AxolotlStore axolotlStore;
public MmsCipher(AxolotlStore axolotlStore) {
this.axolotlStore = axolotlStore;
}
public MultimediaMessagePdu decrypt(Context context, MultimediaMessagePdu pdu)
throws InvalidMessageException, LegacyMessageException, DuplicateMessageException,
NoSessionException
{
try {
SessionCipher sessionCipher = new SessionCipher(axolotlStore, new AxolotlAddress(pdu.getFrom().getString(), TextSecureAddress.DEFAULT_DEVICE_ID));
Optional<byte[]> ciphertext = getEncryptedData(pdu);
if (!ciphertext.isPresent()) {
throw new InvalidMessageException("No ciphertext present!");
}
byte[] decodedCiphertext = textTransport.getDecodedMessage(ciphertext.get());
byte[] plaintext;
if (decodedCiphertext == null) {
throw new InvalidMessageException("failed to decode ciphertext");
}
try {
plaintext = sessionCipher.decrypt(new WhisperMessage(decodedCiphertext));
} catch (InvalidMessageException e) {
// NOTE - For some reason, Sprint seems to append a single character to the
// end of message text segments. I don't know why, so here we just try
// truncating the message by one if the MAC fails.
if (ciphertext.get().length > 2) {
Log.w(TAG, "Attempting truncated decrypt...");
byte[] truncated = Util.trim(ciphertext.get(), ciphertext.get().length - 1);
decodedCiphertext = textTransport.getDecodedMessage(truncated);
plaintext = sessionCipher.decrypt(new WhisperMessage(decodedCiphertext));
} else {
throw e;
}
}
MultimediaMessagePdu plaintextGenericPdu = (MultimediaMessagePdu) new PduParser(plaintext).parse();
return new RetrieveConf(plaintextGenericPdu.getPduHeaders(), plaintextGenericPdu.getBody());
} catch (IOException e) {
throw new InvalidMessageException(e);
}
}
public SendReq encrypt(Context context, SendReq message)
throws NoSessionException, RecipientFormattingException, UndeliverableMessageException
{
EncodedStringValue[] encodedRecipient = message.getTo();
String recipientString = encodedRecipient[0].getString();
byte[] pduBytes = new PduComposer(context, message).make();
if (pduBytes == null) {
throw new UndeliverableMessageException("PDU composition failed, null payload");
}
if (!axolotlStore.containsSession(new AxolotlAddress(recipientString, TextSecureAddress.DEFAULT_DEVICE_ID))) {
throw new NoSessionException("No session for: " + recipientString);
}
SessionCipher cipher = new SessionCipher(axolotlStore, new AxolotlAddress(recipientString, TextSecureAddress.DEFAULT_DEVICE_ID));
CiphertextMessage ciphertextMessage = cipher.encrypt(pduBytes);
byte[] encryptedPduBytes = textTransport.getEncodedMessage(ciphertextMessage.serialize());
PduBody body = new PduBody();
PduPart part = new PduPart();
SendReq encryptedPdu = new SendReq(message.getPduHeaders(), body);
part.setContentId((System.currentTimeMillis()+"").getBytes());
part.setContentType(ContentType.TEXT_PLAIN.getBytes());
part.setName((System.currentTimeMillis()+"").getBytes());
part.setData(encryptedPduBytes);
body.addPart(part);
encryptedPdu.setSubject(new EncodedStringValue(WirePrefix.calculateEncryptedMmsSubject()));
encryptedPdu.setBody(body);
return encryptedPdu;
}
private Optional<byte[]> getEncryptedData(MultimediaMessagePdu pdu) {
for (int i=0;i<pdu.getBody().getPartsNum();i++) {
if (new String(pdu.getBody().getPart(i).getContentType()).equals(ContentType.TEXT_PLAIN)) {
return Optional.of(pdu.getBody().getPart(i).getData());
}
}
return Optional.absent();
}
}

View File

@@ -15,6 +15,10 @@ public class SecurityEvent {
public static final String SECURITY_UPDATE_EVENT = "org.thoughtcrime.securesms.KEY_EXCHANGE_UPDATE";
public static void broadcastSecurityUpdateEvent(Context context) {
broadcastSecurityUpdateEvent(context, -2);
}
public static void broadcastSecurityUpdateEvent(Context context, long threadId) {
Intent intent = new Intent(SECURITY_UPDATE_EVENT);
intent.putExtra("thread_id", threadId);

View File

@@ -1,128 +0,0 @@
package org.thoughtcrime.securesms.crypto;
import android.content.Context;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.sms.IncomingEncryptedMessage;
import org.thoughtcrime.securesms.sms.IncomingKeyExchangeMessage;
import org.thoughtcrime.securesms.sms.IncomingPreKeyBundleMessage;
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
import org.thoughtcrime.securesms.sms.OutgoingKeyExchangeMessage;
import org.thoughtcrime.securesms.sms.OutgoingPrekeyBundleMessage;
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
import org.thoughtcrime.securesms.sms.SmsTransportDetails;
import org.whispersystems.libaxolotl.AxolotlAddress;
import org.whispersystems.libaxolotl.DuplicateMessageException;
import org.whispersystems.libaxolotl.InvalidKeyException;
import org.whispersystems.libaxolotl.InvalidKeyIdException;
import org.whispersystems.libaxolotl.InvalidMessageException;
import org.whispersystems.libaxolotl.InvalidVersionException;
import org.whispersystems.libaxolotl.LegacyMessageException;
import org.whispersystems.libaxolotl.NoSessionException;
import org.whispersystems.libaxolotl.SessionBuilder;
import org.whispersystems.libaxolotl.SessionCipher;
import org.whispersystems.libaxolotl.StaleKeyExchangeException;
import org.whispersystems.libaxolotl.UntrustedIdentityException;
import org.whispersystems.libaxolotl.protocol.CiphertextMessage;
import org.whispersystems.libaxolotl.protocol.KeyExchangeMessage;
import org.whispersystems.libaxolotl.protocol.PreKeyWhisperMessage;
import org.whispersystems.libaxolotl.protocol.WhisperMessage;
import org.whispersystems.libaxolotl.state.AxolotlStore;
import org.whispersystems.textsecure.api.push.TextSecureAddress;
import java.io.IOException;
public class SmsCipher {
private final SmsTransportDetails transportDetails = new SmsTransportDetails();
private final AxolotlStore axolotlStore;
public SmsCipher(AxolotlStore axolotlStore) {
this.axolotlStore = axolotlStore;
}
public IncomingTextMessage decrypt(Context context, IncomingTextMessage message)
throws LegacyMessageException, InvalidMessageException,
DuplicateMessageException, NoSessionException
{
try {
byte[] decoded = transportDetails.getDecodedMessage(message.getMessageBody().getBytes());
WhisperMessage whisperMessage = new WhisperMessage(decoded);
SessionCipher sessionCipher = new SessionCipher(axolotlStore, new AxolotlAddress(message.getSender(), TextSecureAddress.DEFAULT_DEVICE_ID));
byte[] padded = sessionCipher.decrypt(whisperMessage);
byte[] plaintext = transportDetails.getStrippedPaddingMessageBody(padded);
if (message.isEndSession() && "TERMINATE".equals(new String(plaintext))) {
axolotlStore.deleteSession(new AxolotlAddress(message.getSender(), TextSecureAddress.DEFAULT_DEVICE_ID));
}
return message.withMessageBody(new String(plaintext));
} catch (IOException e) {
throw new InvalidMessageException(e);
}
}
public IncomingEncryptedMessage decrypt(Context context, IncomingPreKeyBundleMessage message)
throws InvalidVersionException, InvalidMessageException, DuplicateMessageException,
UntrustedIdentityException, LegacyMessageException
{
try {
byte[] decoded = transportDetails.getDecodedMessage(message.getMessageBody().getBytes());
PreKeyWhisperMessage preKeyMessage = new PreKeyWhisperMessage(decoded);
SessionCipher sessionCipher = new SessionCipher(axolotlStore, new AxolotlAddress(message.getSender(), TextSecureAddress.DEFAULT_DEVICE_ID));
byte[] padded = sessionCipher.decrypt(preKeyMessage);
byte[] plaintext = transportDetails.getStrippedPaddingMessageBody(padded);
return new IncomingEncryptedMessage(message, new String(plaintext));
} catch (IOException | InvalidKeyException | InvalidKeyIdException e) {
throw new InvalidMessageException(e);
}
}
public OutgoingTextMessage encrypt(OutgoingTextMessage message) throws NoSessionException {
byte[] paddedBody = transportDetails.getPaddedMessageBody(message.getMessageBody().getBytes());
String recipientNumber = message.getRecipients().getPrimaryRecipient().getNumber();
if (!axolotlStore.containsSession(new AxolotlAddress(recipientNumber, TextSecureAddress.DEFAULT_DEVICE_ID))) {
throw new NoSessionException("No session for: " + recipientNumber);
}
SessionCipher cipher = new SessionCipher(axolotlStore, new AxolotlAddress(recipientNumber, TextSecureAddress.DEFAULT_DEVICE_ID));
CiphertextMessage ciphertextMessage = cipher.encrypt(paddedBody);
String encodedCiphertext = new String(transportDetails.getEncodedMessage(ciphertextMessage.serialize()));
if (ciphertextMessage.getType() == CiphertextMessage.PREKEY_TYPE) {
return new OutgoingPrekeyBundleMessage(message, encodedCiphertext);
} else {
return message.withBody(encodedCiphertext);
}
}
public OutgoingKeyExchangeMessage process(Context context, IncomingKeyExchangeMessage message)
throws UntrustedIdentityException, StaleKeyExchangeException,
InvalidVersionException, LegacyMessageException, InvalidMessageException
{
try {
Recipient recipient = RecipientFactory.getRecipientsFromString(context, message.getSender(), false).getPrimaryRecipient();
AxolotlAddress axolotlAddress = new AxolotlAddress(message.getSender(), TextSecureAddress.DEFAULT_DEVICE_ID);
KeyExchangeMessage exchangeMessage = new KeyExchangeMessage(transportDetails.getDecodedMessage(message.getMessageBody().getBytes()));
SessionBuilder sessionBuilder = new SessionBuilder(axolotlStore, axolotlAddress);
KeyExchangeMessage response = sessionBuilder.process(exchangeMessage);
if (response != null) {
byte[] serializedResponse = transportDetails.getEncodedMessage(response.serialize());
return new OutgoingKeyExchangeMessage(recipient, new String(serializedResponse));
} else {
return null;
}
} catch (IOException | InvalidKeyException e) {
throw new InvalidMessageException(e);
}
}
}