diff --git a/library/src/org/whispersystems/textsecure/Release.java b/library/src/org/whispersystems/textsecure/Release.java index 13d5b78824..07cebf4ffa 100644 --- a/library/src/org/whispersystems/textsecure/Release.java +++ b/library/src/org/whispersystems/textsecure/Release.java @@ -1,7 +1,7 @@ package org.whispersystems.textsecure; public class Release { - public static final String PUSH_SERVICE_URL = "https://gcm.textsecure.whispersystems.org"; -// public static final String PUSH_SERVICE_URL = "http://192.168.1.135:8080"; - public static final boolean ENFORCE_SSL = true; +// public static final String PUSH_SERVICE_URL = "https://gcm.textsecure.whispersystems.org"; + public static final String PUSH_SERVICE_URL = "http://192.168.1.135:8080"; + public static final boolean ENFORCE_SSL = false; } diff --git a/library/src/org/whispersystems/textsecure/crypto/IdentityKeyPair.java b/library/src/org/whispersystems/textsecure/crypto/IdentityKeyPair.java new file mode 100644 index 0000000000..643e752f59 --- /dev/null +++ b/library/src/org/whispersystems/textsecure/crypto/IdentityKeyPair.java @@ -0,0 +1,43 @@ +/** + * 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 . + */ +package org.whispersystems.textsecure.crypto; + +import org.spongycastle.crypto.params.ECPrivateKeyParameters; + +/** + * Holder for public and private identity key pair. + * + * @author Moxie Marlinspike + */ +public class IdentityKeyPair { + + private final IdentityKey publicKey; + private final ECPrivateKeyParameters privateKey; + + public IdentityKeyPair(IdentityKey publicKey, ECPrivateKeyParameters privateKey) { + this.publicKey = publicKey; + this.privateKey = privateKey; + } + + public IdentityKey getPublicKey() { + return publicKey; + } + + public ECPrivateKeyParameters getPrivateKey() { + return privateKey; + } +} diff --git a/src/org/thoughtcrime/securesms/crypto/InvalidVersionException.java b/library/src/org/whispersystems/textsecure/crypto/InvalidVersionException.java similarity index 96% rename from src/org/thoughtcrime/securesms/crypto/InvalidVersionException.java rename to library/src/org/whispersystems/textsecure/crypto/InvalidVersionException.java index 2213ab2a9e..6194b1c3c9 100644 --- a/src/org/thoughtcrime/securesms/crypto/InvalidVersionException.java +++ b/library/src/org/whispersystems/textsecure/crypto/InvalidVersionException.java @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.thoughtcrime.securesms.crypto; +package org.whispersystems.textsecure.crypto; public class InvalidVersionException extends Exception { diff --git a/library/src/org/whispersystems/textsecure/crypto/PreKeyPair.java b/library/src/org/whispersystems/textsecure/crypto/PreKeyPair.java index 0e24d904bc..3b03500560 100644 --- a/library/src/org/whispersystems/textsecure/crypto/PreKeyPair.java +++ b/library/src/org/whispersystems/textsecure/crypto/PreKeyPair.java @@ -33,6 +33,10 @@ public class PreKeyPair { return publicKey; } + public AsymmetricCipherKeyPair getKeyPair() { + return new AsymmetricCipherKeyPair(publicKey.getPublicKey(), privateKey); + } + public byte[] serialize() { byte[] publicKeyBytes = publicKey.serialize(); byte[] privateKeyBytes = masterCipher.encryptKey(privateKey); diff --git a/library/src/org/whispersystems/textsecure/crypto/SessionCipher.java b/library/src/org/whispersystems/textsecure/crypto/SessionCipher.java index c93cd8831b..24fdfa2477 100644 --- a/library/src/org/whispersystems/textsecure/crypto/SessionCipher.java +++ b/library/src/org/whispersystems/textsecure/crypto/SessionCipher.java @@ -42,6 +42,9 @@ import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; /** * This is where the session encryption magic happens. Implements a compressed version of the OTR protocol. @@ -58,35 +61,42 @@ public class SessionCipher { public static final int ENCRYPTED_MESSAGE_OVERHEAD = EncryptedMessage.HEADER_LENGTH + MessageMac.MAC_LENGTH; - public SessionCipherContext getEncryptionContext(Context context, MasterSecret masterSecret, + public SessionCipherContext getEncryptionContext(Context context, + MasterSecret masterSecret, + IdentityKeyPair localIdentityKey, CanonicalRecipientAddress recipient) { try { KeyRecords records = getKeyRecords(context, masterSecret, recipient); int localKeyId = records.getLocalKeyRecord().getCurrentKeyPair().getId(); int remoteKeyId = records.getRemoteKeyRecord().getCurrentRemoteKey().getId(); - SessionKey sessionKey = getSessionKey(masterSecret, Cipher.ENCRYPT_MODE, records, localKeyId, remoteKeyId); + int negotiatedVersion = records.getSessionRecord().getSessionVersion(); + SessionKey sessionKey = getSessionKey(masterSecret, Cipher.ENCRYPT_MODE, negotiatedVersion, localIdentityKey, records, localKeyId, remoteKeyId); PublicKey nextKey = records.getLocalKeyRecord().getNextKeyPair().getPublicKey(); int counter = records.getSessionRecord().getCounter(); - int negotiatedVersion = records.getSessionRecord().getSessionVersion(); - return new SessionCipherContext(records, sessionKey, localKeyId, remoteKeyId, nextKey, counter, negotiatedVersion); + + return new SessionCipherContext(records, sessionKey, localKeyId, remoteKeyId, + nextKey, counter, negotiatedVersion, negotiatedVersion); } catch (InvalidKeyIdException e) { throw new IllegalArgumentException(e); } } public SessionCipherContext getDecryptionContext(Context context, MasterSecret masterSecret, + IdentityKeyPair localIdentityKey, CanonicalRecipientAddress recipient, int senderKeyId, int recipientKeyId, PublicKey nextKey, int counter, - int negotiatedVersion) + int messageVersion, int negotiatedVersion) throws InvalidMessageException { try { KeyRecords records = getKeyRecords(context, masterSecret, recipient); - SessionKey sessionKey = getSessionKey(masterSecret, Cipher.DECRYPT_MODE, records, recipientKeyId, senderKeyId); - return new SessionCipherContext(records, sessionKey, senderKeyId, recipientKeyId, nextKey, counter, negotiatedVersion); + SessionKey sessionKey = getSessionKey(masterSecret, Cipher.DECRYPT_MODE, messageVersion, localIdentityKey, records, recipientKeyId, senderKeyId); + return new SessionCipherContext(records, sessionKey, senderKeyId, + recipientKeyId, nextKey, counter, + messageVersion, negotiatedVersion); } catch (InvalidKeyIdException e) { throw new InvalidMessageException(e); } @@ -193,12 +203,12 @@ public class SessionCipher { } } - private SecretKeySpec deriveCipherSecret(int mode, BigInteger sharedSecret, + private SecretKeySpec deriveCipherSecret(int mode, List sharedSecret, KeyRecords records, int localKeyId, int remoteKeyId) throws InvalidKeyIdException { - byte[] sharedSecretBytes = sharedSecret.toByteArray(); + byte[] sharedSecretBytes = concatenateSharedSecrets(sharedSecret); byte[] derivedBytes = deriveBytes(sharedSecretBytes, 16 * 2); byte[] cipherSecret = new byte[16]; @@ -213,18 +223,40 @@ public class SessionCipher { return new SecretKeySpec(cipherSecret, "AES"); } + + private byte[] concatenateSharedSecrets(List sharedSecrets) { + int totalByteSize = 0; + List byteValues = new LinkedList(); + + for (BigInteger sharedSecret : sharedSecrets) { + byte[] byteValue = sharedSecret.toByteArray(); + totalByteSize += byteValue.length; + byteValues.add(byteValue); + } + + byte[] combined = new byte[totalByteSize]; + int offset = 0; + + for (byte[] byteValue : byteValues) { + System.arraycopy(byteValue, 0, combined, offset, byteValue.length); + offset += byteValue.length; + } + + return combined; + } private boolean isLowEnd(KeyRecords records, int localKeyId, int remoteKeyId) throws InvalidKeyIdException { ECPublicKeyParameters localPublic = records.getLocalKeyRecord().getKeyPairForId(localKeyId).getPublicKey().getKey(); ECPublicKeyParameters remotePublic = records.getRemoteKeyRecord().getKeyForId(remoteKeyId).getKey(); + BigInteger local = localPublic.getQ().getX().toBigInteger(); BigInteger remote = remotePublic.getQ().getX().toBigInteger(); - + return local.compareTo(remote) < 0; } - + private byte[] deriveBytes(byte[] seed, int bytesNeeded) { MessageDigest md; @@ -246,7 +278,10 @@ public class SessionCipher { return md.digest(); } - private SessionKey getSessionKey(MasterSecret masterSecret, int mode, KeyRecords records, + private SessionKey getSessionKey(MasterSecret masterSecret, int mode, + int messageVersion, + IdentityKeyPair localIdentityKey, + KeyRecords records, int localKeyId, int remoteKeyId) throws InvalidKeyIdException { @@ -256,23 +291,41 @@ public class SessionCipher { if (sessionKey != null) return sessionKey; - BigInteger sharedSecret = calculateSharedSecret(records, localKeyId, remoteKeyId); - SecretKeySpec cipherKey = deriveCipherSecret(mode, sharedSecret, records, localKeyId, remoteKeyId); - SecretKeySpec macKey = deriveMacSecret(cipherKey); + List sharedSecret = calculateSharedSecret(messageVersion, localIdentityKey, records, localKeyId, remoteKeyId); + SecretKeySpec cipherKey = deriveCipherSecret(mode, sharedSecret, records, localKeyId, remoteKeyId); + SecretKeySpec macKey = deriveMacSecret(cipherKey); return new SessionKey(localKeyId, remoteKeyId, cipherKey, macKey, masterSecret); } - private BigInteger calculateSharedSecret(KeyRecords records, int localKeyId, int remoteKeyId) + private List calculateSharedSecret(int messageVersion, + IdentityKeyPair localIdentityKey, + KeyRecords records, + int localKeyId, int remoteKeyId) throws InvalidKeyIdException { - ECDHBasicAgreement agreement = new ECDHBasicAgreement(); - AsymmetricCipherKeyPair localKeyPair = records.getLocalKeyRecord().getKeyPairForId(localKeyId).getKeyPair(); - ECPublicKeyParameters remoteKey = records.getRemoteKeyRecord().getKeyForId(remoteKeyId).getKey(); - - agreement.init(localKeyPair.getPrivate()); + KeyPair localKeyPair = records.getLocalKeyRecord().getKeyPairForId(localKeyId); + ECPublicKeyParameters remoteKey = records.getRemoteKeyRecord().getKeyForId(remoteKeyId).getKey(); + IdentityKey remoteIdentityKey = records.getSessionRecord().getIdentityKey(); - return KeyUtil.calculateAgreement(agreement, remoteKey); + if (isInitiallyExchangedKeys(records, localKeyId, remoteKeyId) && + messageVersion >= EncryptedMessage.CRADLE_AGREEMENT_VERSION) + { + return SharedSecretCalculator.calculateSharedSecret(localKeyPair, localIdentityKey, + remoteKey, remoteIdentityKey); + } else { + return SharedSecretCalculator.calculateSharedSecret(localKeyPair, remoteKey); + } + } + + private boolean isInitiallyExchangedKeys(KeyRecords records, int localKeyId, int remoteKeyId) + throws InvalidKeyIdException + { + byte[] localFingerprint = records.getSessionRecord().getLocalFingerprint(); + byte[] remoteFingerprint = records.getSessionRecord().getRemoteFingerprint(); + + return Arrays.equals(localFingerprint, records.getLocalKeyRecord().getKeyPairForId(localKeyId).getPublicKey().getFingerprintBytes()) && + Arrays.equals(remoteFingerprint, records.getRemoteKeyRecord().getKeyForId(remoteKeyId).getFingerprintBytes()); } private KeyRecords getKeyRecords(Context context, MasterSecret masterSecret, @@ -285,6 +338,7 @@ public class SessionCipher { } private static class KeyRecords { + private final LocalKeyRecord localKeyRecord; private final RemoteKeyRecord remoteKeyRecord; private final SessionRecord sessionRecord; @@ -309,6 +363,7 @@ public class SessionCipher { } public static class SessionCipherContext { + private final LocalKeyRecord localKeyRecord; private final RemoteKeyRecord remoteKeyRecord; private final SessionRecord sessionRecord; @@ -317,6 +372,7 @@ public class SessionCipher { private final int recipientKeyId; private final PublicKey nextKey; private final int counter; + private final int messageVersion; private final int negotiatedVersion; public SessionCipherContext(KeyRecords records, @@ -325,6 +381,7 @@ public class SessionCipher { int receiverKeyId, PublicKey nextKey, int counter, + int messageVersion, int negotiatedVersion) { this.localKeyRecord = records.getLocalKeyRecord(); @@ -335,6 +392,7 @@ public class SessionCipher { this.recipientKeyId = receiverKeyId; this.nextKey = nextKey; this.counter = counter; + this.messageVersion = messageVersion; this.negotiatedVersion = negotiatedVersion; } @@ -373,6 +431,10 @@ public class SessionCipher { public int getNegotiatedVersion() { return negotiatedVersion; } + + public int getMessageVersion() { + return messageVersion; + } } } diff --git a/library/src/org/whispersystems/textsecure/crypto/SharedSecretCalculator.java b/library/src/org/whispersystems/textsecure/crypto/SharedSecretCalculator.java new file mode 100644 index 0000000000..2c51df6a41 --- /dev/null +++ b/library/src/org/whispersystems/textsecure/crypto/SharedSecretCalculator.java @@ -0,0 +1,68 @@ +package org.whispersystems.textsecure.crypto; + +import android.util.Log; + +import org.spongycastle.crypto.CipherParameters; +import org.spongycastle.crypto.agreement.ECDHBasicAgreement; +import org.spongycastle.crypto.params.ECPublicKeyParameters; + +import java.math.BigInteger; +import java.util.LinkedList; +import java.util.List; + +public class SharedSecretCalculator { + + public static List calculateSharedSecret(KeyPair localKeyPair, + IdentityKeyPair localIdentityKeyPair, + ECPublicKeyParameters remoteKey, + IdentityKey remoteIdentityKey) + { + Log.w("SharedSecretCalculator", "Calculating shared secret with cradle agreement..."); + List results = new LinkedList(); + + if (isLowEnd(localKeyPair.getPublicKey().getKey(), remoteKey)) { + results.add(calculateAgreement(localIdentityKeyPair.getPrivateKey(), remoteKey)); + + results.add(calculateAgreement(localKeyPair.getKeyPair().getPrivate(), + remoteIdentityKey.getPublicKeyParameters())); + } else { + results.add(calculateAgreement(localKeyPair.getKeyPair().getPrivate(), + remoteIdentityKey.getPublicKeyParameters())); + + results.add(calculateAgreement(localIdentityKeyPair.getPrivateKey(), remoteKey)); + } + + results.add(calculateAgreement(localKeyPair.getKeyPair().getPrivate(), remoteKey)); + return results; + } + + public static List calculateSharedSecret(KeyPair localKeyPair, + ECPublicKeyParameters remoteKey) + { + Log.w("SharedSecretCalculator", "Calculating shared secret with standard agreement..."); + List results = new LinkedList(); + results.add(calculateAgreement(localKeyPair.getKeyPair().getPrivate(), remoteKey)); + + return results; + } + + private static BigInteger calculateAgreement(CipherParameters privateKey, + ECPublicKeyParameters publicKey) + { + ECDHBasicAgreement agreement = new ECDHBasicAgreement(); + agreement.init(privateKey); + + return KeyUtil.calculateAgreement(agreement, publicKey); + } + + + private static boolean isLowEnd(ECPublicKeyParameters localPublic, + ECPublicKeyParameters remotePublic) + { + BigInteger local = localPublic.getQ().getX().toBigInteger(); + BigInteger remote = remotePublic.getQ().getX().toBigInteger(); + + return local.compareTo(remote) < 0; + } + +} diff --git a/library/src/org/whispersystems/textsecure/crypto/protocol/EncryptedMessage.java b/library/src/org/whispersystems/textsecure/crypto/protocol/EncryptedMessage.java index 13ad15ecbc..9383a373bd 100644 --- a/library/src/org/whispersystems/textsecure/crypto/protocol/EncryptedMessage.java +++ b/library/src/org/whispersystems/textsecure/crypto/protocol/EncryptedMessage.java @@ -17,16 +17,20 @@ package org.whispersystems.textsecure.crypto.protocol; import android.content.Context; +import android.util.Log; +import org.whispersystems.textsecure.crypto.IdentityKeyPair; import org.whispersystems.textsecure.crypto.InvalidKeyException; import org.whispersystems.textsecure.crypto.InvalidMessageException; import org.whispersystems.textsecure.crypto.MasterSecret; +import org.whispersystems.textsecure.crypto.MessageMac; import org.whispersystems.textsecure.crypto.PublicKey; import org.whispersystems.textsecure.crypto.SessionCipher; import org.whispersystems.textsecure.crypto.SessionCipher.SessionCipherContext; import org.whispersystems.textsecure.crypto.TransportDetails; import org.whispersystems.textsecure.storage.CanonicalRecipientAddress; import org.whispersystems.textsecure.util.Conversions; +import org.whispersystems.textsecure.util.Hex; import java.io.IOException; import java.nio.ByteBuffer; @@ -39,29 +43,35 @@ import java.nio.ByteBuffer; public class EncryptedMessage { - public static final int SUPPORTED_VERSION = 1; + public static final int SUPPORTED_VERSION = 2; + public static final int CRADLE_AGREEMENT_VERSION = 2; - private static final int VERSION_LENGTH = 1; + static final int VERSION_LENGTH = 1; private static final int SENDER_KEY_ID_LENGTH = 3; private static final int RECEIVER_KEY_ID_LENGTH = 3; - private static final int NEXT_KEY_LENGTH = PublicKey.KEY_SIZE; + 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; - private static final int VERSION_OFFSET = 0; + static final int VERSION_OFFSET = 0; private static final int SENDER_KEY_ID_OFFSET = VERSION_OFFSET + VERSION_LENGTH; - private static final int RECEIVER_KEY_ID_OFFSET = SENDER_KEY_ID_OFFSET + SENDER_KEY_ID_LENGTH; - private static final int NEXT_KEY_OFFSET = RECEIVER_KEY_ID_OFFSET + RECEIVER_KEY_ID_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; private static final int COUNTER_OFFSET = NEXT_KEY_OFFSET + NEXT_KEY_LENGTH; private static final int TEXT_OFFSET = COUNTER_OFFSET + COUNTER_LENGTH; private final Context context; private final MasterSecret masterSecret; + private final IdentityKeyPair localIdentityKey; private final TransportDetails transportDetails; - public EncryptedMessage(Context context, MasterSecret masterSecret, TransportDetails transportDetails) { + public EncryptedMessage(Context context, MasterSecret masterSecret, + IdentityKeyPair localIdentityKey, + TransportDetails transportDetails) + { this.context = context.getApplicationContext(); this.masterSecret = masterSecret; + this.localIdentityKey = localIdentityKey; this.transportDetails = transportDetails; } @@ -69,7 +79,7 @@ public class EncryptedMessage { synchronized (SessionCipher.CIPHER_LOCK) { byte[] paddedBody = transportDetails.getPaddedMessageBody(plaintext); SessionCipher sessionCipher = new SessionCipher(); - SessionCipherContext sessionContext = sessionCipher.getEncryptionContext(context, masterSecret, recipient); + SessionCipherContext sessionContext = sessionCipher.getEncryptionContext(context, masterSecret, localIdentityKey, recipient); byte[] ciphertextBody = sessionCipher.encrypt(sessionContext, paddedBody); byte[] formattedCiphertext = getFormattedCiphertext(sessionContext, ciphertextBody); byte[] ciphertextMessage = sessionCipher.mac(sessionContext, formattedCiphertext); @@ -84,25 +94,33 @@ public class EncryptedMessage { synchronized (SessionCipher.CIPHER_LOCK) { try { byte[] decodedMessage = transportDetails.getDecodedMessage(ciphertext); - int messageVersion = getMessageVersion(decodedMessage); + + if (decodedMessage.length <= HEADER_LENGTH) { + throw new InvalidMessageException("Message is shorter than headers"); + } + + int messageVersion = getMessageVersion(decodedMessage); if (messageVersion > SUPPORTED_VERSION) { throw new InvalidMessageException("Unsupported version: " + messageVersion); } - int supportedVersion = getSupportedVersion(decodedMessage); - int receiverKeyId = getReceiverKeyId(decodedMessage); - int senderKeyId = getSenderKeyId(decodedMessage); - int counter = getCiphertextCounter(decodedMessage); - byte[] ciphertextBody = getMessageBody(decodedMessage); - PublicKey nextRemoteKey = getNextRemoteKey(decodedMessage); - int version = Math.min(supportedVersion, SUPPORTED_VERSION); - SessionCipher sessionCipher = new SessionCipher(); - SessionCipherContext sessionContext = sessionCipher.getDecryptionContext(context, masterSecret, - recipient, senderKeyId, - receiverKeyId, - nextRemoteKey, - counter, version); + int supportedVersion = getSupportedVersion(decodedMessage); + int receiverKeyId = getReceiverKeyId(decodedMessage); + int senderKeyId = getSenderKeyId(decodedMessage); + int counter = getCiphertextCounter(decodedMessage); + byte[] ciphertextBody = getMessageBody(decodedMessage); + PublicKey nextRemoteKey = getNextRemoteKey(decodedMessage); + int negotiatedVersion = Math.min(supportedVersion, SUPPORTED_VERSION); + SessionCipher sessionCipher = new SessionCipher(); + SessionCipherContext sessionContext = sessionCipher.getDecryptionContext(context, masterSecret, + localIdentityKey, + recipient, senderKeyId, + receiverKeyId, + nextRemoteKey, + counter, + messageVersion, + negotiatedVersion); sessionCipher.verifyMac(sessionContext, decodedMessage); @@ -155,7 +173,7 @@ public class EncryptedMessage { } private byte[] getMessageBody(byte[] message) { - byte[] body = new byte[message.length - HEADER_LENGTH]; + byte[] body = new byte[message.length - HEADER_LENGTH - MessageMac.MAC_LENGTH]; System.arraycopy(message, TEXT_OFFSET, body, 0, body.length); return body; diff --git a/library/src/org/whispersystems/textsecure/crypto/protocol/PreKeyBundleMessage.java b/library/src/org/whispersystems/textsecure/crypto/protocol/PreKeyBundleMessage.java new file mode 100644 index 0000000000..e59091f3bd --- /dev/null +++ b/library/src/org/whispersystems/textsecure/crypto/protocol/PreKeyBundleMessage.java @@ -0,0 +1,125 @@ +/** + * 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 . + */ +package org.whispersystems.textsecure.crypto.protocol; + +import org.whispersystems.textsecure.crypto.IdentityKey; +import org.whispersystems.textsecure.crypto.InvalidKeyException; +import org.whispersystems.textsecure.crypto.InvalidVersionException; +import org.whispersystems.textsecure.crypto.PublicKey; +import org.whispersystems.textsecure.util.Base64; +import org.whispersystems.textsecure.util.Conversions; + +import java.io.IOException; + +/** + * Class responsible for parsing and constructing PreKeyBundle messages. + * + * The class takes an existing encrypted message and bundles in the necessary + * additional information for a prekeybundle, namely the addition of the local + * identity key. + */ +public class PreKeyBundleMessage { + + private static final int VERSION_LENGTH = EncryptedMessage.VERSION_LENGTH; + private static final int IDENTITY_KEY_LENGTH = IdentityKey.SIZE; + public static final int HEADER_LENGTH = IDENTITY_KEY_LENGTH + EncryptedMessage.HEADER_LENGTH; + + private static final int VERSION_OFFSET = EncryptedMessage.VERSION_OFFSET; + private static final int IDENTITY_KEY_OFFSET = VERSION_OFFSET + EncryptedMessage.VERSION_LENGTH; + private static final int PUBLIC_KEY_OFFSET = IDENTITY_KEY_LENGTH + EncryptedMessage.NEXT_KEY_OFFSET; + private static final int PREKEY_ID_OFFSET = IDENTITY_KEY_LENGTH + EncryptedMessage.RECEIVER_KEY_ID_OFFSET; + + private final byte[] messageBytes; + + private final int supportedVersion; + private final int messageVersion; + private final int preKeyId; + private final IdentityKey identityKey; + private final PublicKey publicKey; + private final byte[] bundledMessage; + + public PreKeyBundleMessage(String message) throws InvalidKeyException, InvalidVersionException { + try { + this.messageBytes = Base64.decodeWithoutPadding(message); + this.messageVersion = Conversions.highBitsToInt(this.messageBytes[VERSION_OFFSET]); + + if (messageVersion > EncryptedMessage.SUPPORTED_VERSION) + throw new InvalidVersionException("Key exchange with version: " + messageVersion + + " but we only support: " + EncryptedMessage.SUPPORTED_VERSION); + + this.supportedVersion = Conversions.lowBitsToInt(messageBytes[VERSION_OFFSET]); + this.publicKey = new PublicKey(messageBytes, PUBLIC_KEY_OFFSET); + this.identityKey = new IdentityKey(messageBytes, IDENTITY_KEY_OFFSET); + this.preKeyId = Conversions.byteArrayToMedium(messageBytes, PREKEY_ID_OFFSET); + this.bundledMessage = new byte[messageBytes.length - IDENTITY_KEY_LENGTH]; + + + this.bundledMessage[VERSION_OFFSET] = this.messageBytes[VERSION_OFFSET]; + System.arraycopy(messageBytes, IDENTITY_KEY_OFFSET+IDENTITY_KEY_LENGTH, bundledMessage, VERSION_OFFSET+VERSION_LENGTH, bundledMessage.length-VERSION_LENGTH); + } catch (IOException e) { + throw new InvalidKeyException(e); + } + } + + public PreKeyBundleMessage(IdentityKey identityKey, byte[] bundledMessage) { + try { + this.supportedVersion = EncryptedMessage.SUPPORTED_VERSION; + this.messageVersion = EncryptedMessage.SUPPORTED_VERSION; + this.identityKey = identityKey; + this.publicKey = new PublicKey(bundledMessage, EncryptedMessage.NEXT_KEY_OFFSET); + this.preKeyId = Conversions.byteArrayToMedium(bundledMessage, EncryptedMessage.RECEIVER_KEY_ID_OFFSET); + this.bundledMessage = bundledMessage; + this.messageBytes = new byte[IDENTITY_KEY_LENGTH + bundledMessage.length]; + + byte[] identityKeyBytes = identityKey.serialize(); + + messageBytes[VERSION_OFFSET] = bundledMessage[VERSION_OFFSET]; + System.arraycopy(identityKeyBytes, 0, messageBytes, IDENTITY_KEY_OFFSET, identityKeyBytes.length); + System.arraycopy(bundledMessage, VERSION_OFFSET+VERSION_LENGTH, messageBytes, IDENTITY_KEY_OFFSET+IDENTITY_KEY_LENGTH, bundledMessage.length-VERSION_LENGTH); + } catch (InvalidKeyException e) { + throw new AssertionError(e); + } + } + + public String serialize() { + return Base64.encodeBytesWithoutPadding(this.messageBytes); + } + + public int getSupportedVersion() { + return supportedVersion; + } + + public int getMessageVersion() { + return messageVersion; + } + + public IdentityKey getIdentityKey() { + return identityKey; + } + + public PublicKey getPublicKey() { + return publicKey; + } + + public String getBundledMessage() { + return Base64.encodeBytesWithoutPadding(bundledMessage); + } + + public int getPreKeyId() { + return preKeyId; + } +} diff --git a/library/src/org/whispersystems/textsecure/push/IncomingPushMessage.java b/library/src/org/whispersystems/textsecure/push/IncomingPushMessage.java index a10f5cd0d7..8a146a68bc 100644 --- a/library/src/org/whispersystems/textsecure/push/IncomingPushMessage.java +++ b/library/src/org/whispersystems/textsecure/push/IncomingPushMessage.java @@ -20,6 +20,7 @@ public class IncomingPushMessage implements Parcelable { } }; + private int type; private String source; private List destinations; private String messageText; @@ -27,8 +28,9 @@ public class IncomingPushMessage implements Parcelable { private long timestamp; public IncomingPushMessage(String source, List destinations, String messageText, - List attachments, long timestamp) + int type, List attachments, long timestamp) { + this.type = type; this.source = source; this.destinations = destinations; this.messageText = messageText; @@ -84,4 +86,8 @@ public class IncomingPushMessage implements Parcelable { dest.writeList(attachments); dest.writeLong(timestamp); } + + public int getType() { + return type; + } } diff --git a/library/src/org/whispersystems/textsecure/push/OutgoingPushMessage.java b/library/src/org/whispersystems/textsecure/push/OutgoingPushMessage.java index 7d658b96d5..fa0f7a6cc4 100644 --- a/library/src/org/whispersystems/textsecure/push/OutgoingPushMessage.java +++ b/library/src/org/whispersystems/textsecure/push/OutgoingPushMessage.java @@ -5,29 +5,36 @@ import java.util.List; public class OutgoingPushMessage { + public static final int TYPE_MESSAGE = 1; + public static final int TYPE_PREKEYED_MESSAGE = 2; + + private int type; private List destinations; private String messageText; private List attachments; - public OutgoingPushMessage(String destination, String messageText) { + public OutgoingPushMessage(String destination, String messageText, int type) { this.destinations = new LinkedList(); this.attachments = new LinkedList(); this.messageText = messageText; this.destinations.add(destination); + this.type = type; } - public OutgoingPushMessage(List destinations, String messageText) { + public OutgoingPushMessage(List destinations, String messageText, int type) { this.destinations = destinations; this.messageText = messageText; this.attachments = new LinkedList(); + this.type = type; } public OutgoingPushMessage(List destinations, String messageText, - List attachments) + List attachments, int type) { this.destinations = destinations; this.messageText = messageText; this.attachments = attachments; + this.type = type; } public List getDestinations() { @@ -42,4 +49,7 @@ public class OutgoingPushMessage { return attachments; } + public int getType() { + return type; + } } diff --git a/library/src/org/whispersystems/textsecure/push/PushServiceSocket.java b/library/src/org/whispersystems/textsecure/push/PushServiceSocket.java index 16eb40d8d8..e55ef5c6d5 100644 --- a/library/src/org/whispersystems/textsecure/push/PushServiceSocket.java +++ b/library/src/org/whispersystems/textsecure/push/PushServiceSocket.java @@ -78,26 +78,26 @@ public class PushServiceSocket { makeRequest(REGISTER_GCM_PATH, "DELETE", null); } - public void sendMessage(String recipient, String messageText) + public void sendMessage(String recipient, String messageText, int type) throws IOException { - OutgoingPushMessage message = new OutgoingPushMessage(recipient, messageText); + OutgoingPushMessage message = new OutgoingPushMessage(recipient, messageText, type); sendMessage(message); } - public void sendMessage(List recipients, String messageText) + public void sendMessage(List recipients, String messageText, int type) throws IOException { - OutgoingPushMessage message = new OutgoingPushMessage(recipients, messageText); + OutgoingPushMessage message = new OutgoingPushMessage(recipients, messageText, type); sendMessage(message); } public void sendMessage(List recipients, String messageText, - List attachments) + List attachments, int type) throws IOException { List attachmentIds = sendAttachments(attachments); - OutgoingPushMessage message = new OutgoingPushMessage(recipients, messageText, attachmentIds); + OutgoingPushMessage message = new OutgoingPushMessage(recipients, messageText, attachmentIds, type); sendMessage(message); } diff --git a/src/org/thoughtcrime/securesms/ReceiveKeyActivity.java b/src/org/thoughtcrime/securesms/ReceiveKeyActivity.java index 1da034570e..0e38236035 100644 --- a/src/org/thoughtcrime/securesms/ReceiveKeyActivity.java +++ b/src/org/thoughtcrime/securesms/ReceiveKeyActivity.java @@ -31,7 +31,7 @@ import android.widget.Button; import android.widget.TextView; import org.whispersystems.textsecure.crypto.InvalidKeyException; -import org.thoughtcrime.securesms.crypto.InvalidVersionException; +import org.whispersystems.textsecure.crypto.InvalidVersionException; import org.thoughtcrime.securesms.crypto.protocol.KeyExchangeMessage; import org.thoughtcrime.securesms.crypto.KeyExchangeProcessor; import org.whispersystems.textsecure.crypto.MasterSecret; diff --git a/src/org/thoughtcrime/securesms/RegistrationProgressActivity.java b/src/org/thoughtcrime/securesms/RegistrationProgressActivity.java index 0fe5fb924a..dd086efe3d 100644 --- a/src/org/thoughtcrime/securesms/RegistrationProgressActivity.java +++ b/src/org/thoughtcrime/securesms/RegistrationProgressActivity.java @@ -194,7 +194,9 @@ public class RegistrationProgressActivity extends SherlockActivity { intent.putExtra("master_secret", masterSecret); startService(intent); } else { - startActivity(new Intent(this, RegistrationActivity.class)); + Intent intent = new Intent(this, RegistrationActivity.class); + intent.putExtra("master_secret", masterSecret); + startActivity(intent); finish(); } } @@ -408,6 +410,7 @@ public class RegistrationProgressActivity extends SherlockActivity { shutdownService(); Intent activityIntent = new Intent(RegistrationProgressActivity.this, RegistrationActivity.class); + activityIntent.putExtra("master_secret", masterSecret); startActivity(activityIntent); finish(); } diff --git a/src/org/thoughtcrime/securesms/crypto/DecryptingQueue.java b/src/org/thoughtcrime/securesms/crypto/DecryptingQueue.java index 8f50d0c799..738eced2c8 100644 --- a/src/org/thoughtcrime/securesms/crypto/DecryptingQueue.java +++ b/src/org/thoughtcrime/securesms/crypto/DecryptingQueue.java @@ -34,8 +34,11 @@ import org.thoughtcrime.securesms.recipients.RecipientFactory; import org.thoughtcrime.securesms.recipients.RecipientFormattingException; import org.thoughtcrime.securesms.recipients.Recipients; import org.thoughtcrime.securesms.sms.SmsTransportDetails; +import org.whispersystems.textsecure.crypto.IdentityKey; +import org.whispersystems.textsecure.crypto.IdentityKeyPair; import org.whispersystems.textsecure.crypto.InvalidKeyException; import org.whispersystems.textsecure.crypto.InvalidMessageException; +import org.whispersystems.textsecure.crypto.InvalidVersionException; import org.whispersystems.textsecure.crypto.SessionCipher; import org.whispersystems.textsecure.crypto.protocol.EncryptedMessage; import org.whispersystems.textsecure.util.Hex; @@ -195,7 +198,8 @@ public class DecryptingQueue { synchronized (SessionCipher.CIPHER_LOCK) { Log.w("DecryptingQueue", "Decrypting: " + Hex.toString(ciphertextPduBytes)); - EncryptedMessage message = new EncryptedMessage(context, masterSecret, new TextTransport()); + IdentityKeyPair identityKey = IdentityKeyUtil.getIdentityKeyPair(context, masterSecret); + EncryptedMessage message = new EncryptedMessage(context, masterSecret, identityKey, new TextTransport()); try { plaintextPduBytes = message.decrypt(recipient, ciphertextPduBytes); @@ -275,7 +279,9 @@ public class DecryptingQueue { return; } - EncryptedMessage message = new EncryptedMessage(context, masterSecret, new SmsTransportDetails()); + IdentityKeyPair identityKey = IdentityKeyUtil.getIdentityKeyPair(context, masterSecret); + EncryptedMessage message = new EncryptedMessage(context, masterSecret, identityKey, new SmsTransportDetails()); + plaintextBody = new String(message.decrypt(recipient, body.getBytes())); } catch (InvalidMessageException e) { Log.w("DecryptionQueue", e); diff --git a/src/org/thoughtcrime/securesms/crypto/IdentityKeyUtil.java b/src/org/thoughtcrime/securesms/crypto/IdentityKeyUtil.java index 2c2ebde48c..e852539ab6 100644 --- a/src/org/thoughtcrime/securesms/crypto/IdentityKeyUtil.java +++ b/src/org/thoughtcrime/securesms/crypto/IdentityKeyUtil.java @@ -31,6 +31,7 @@ import org.spongycastle.crypto.params.ECPrivateKeyParameters; import org.spongycastle.crypto.params.ECPublicKeyParameters; import org.spongycastle.crypto.signers.ECDSASigner; import org.whispersystems.textsecure.crypto.IdentityKey; +import org.whispersystems.textsecure.crypto.IdentityKeyPair; import org.whispersystems.textsecure.crypto.InvalidKeyException; import org.whispersystems.textsecure.crypto.KeyUtil; import org.whispersystems.textsecure.crypto.MasterCipher; @@ -75,6 +76,22 @@ public class IdentityKeyUtil { return null; } } + + public static IdentityKeyPair getIdentityKeyPair(Context context, MasterSecret masterSecret) { + if (!hasIdentityKey(context)) + return null; + + try { + MasterCipher masterCipher = new MasterCipher(masterSecret); + IdentityKey publicKey = getIdentityKey(context); + byte[] privateKeyBytes = Base64.decode(retrieve(context, IDENTITY_PRIVATE_KEY_PREF)); + ECPrivateKeyParameters privateKey = masterCipher.decryptKey(privateKeyBytes); + + return new IdentityKeyPair(publicKey, privateKey); + } catch (IOException e) { + throw new AssertionError(e); + } + } public static String getFingerprint(Context context) { if (!hasIdentityKey(context)) return null; diff --git a/src/org/thoughtcrime/securesms/crypto/KeyExchangeProcessor.java b/src/org/thoughtcrime/securesms/crypto/KeyExchangeProcessor.java index 7c02a67fa4..9352712bfc 100644 --- a/src/org/thoughtcrime/securesms/crypto/KeyExchangeProcessor.java +++ b/src/org/thoughtcrime/securesms/crypto/KeyExchangeProcessor.java @@ -23,12 +23,16 @@ import android.util.Log; import org.thoughtcrime.securesms.crypto.protocol.KeyExchangeMessage; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.whispersystems.textsecure.crypto.IdentityKey; +import org.whispersystems.textsecure.crypto.KeyPair; import org.whispersystems.textsecure.crypto.KeyUtil; import org.whispersystems.textsecure.crypto.MasterSecret; import org.whispersystems.textsecure.crypto.PublicKey; +import org.whispersystems.textsecure.crypto.protocol.PreKeyBundleMessage; import org.whispersystems.textsecure.crypto.protocol.EncryptedMessage; import org.whispersystems.textsecure.push.PreKeyEntity; +import org.whispersystems.textsecure.storage.InvalidKeyIdException; import org.whispersystems.textsecure.storage.LocalKeyRecord; +import org.whispersystems.textsecure.storage.PreKeyRecord; import org.whispersystems.textsecure.storage.RemoteKeyRecord; import org.whispersystems.textsecure.storage.SessionRecord; import org.thoughtcrime.securesms.recipients.Recipient; @@ -72,6 +76,10 @@ public class KeyExchangeProcessor { return isTrusted(message.getIdentityKey()); } + public boolean isTrusted(PreKeyBundleMessage message) { + return isTrusted(message.getIdentityKey()); + } + public boolean isTrusted(IdentityKey identityKey) { return DatabaseFactory.getIdentityDatabase(context).isValidIdentity(masterSecret, recipient, identityKey); @@ -94,6 +102,40 @@ public class KeyExchangeProcessor { (localKeyRecord.getCurrentKeyPair() != null && localKeyRecord.getCurrentKeyPair().getId() != responseKeyId); } + public void processKeyExchangeMessage(PreKeyBundleMessage message) throws InvalidKeyIdException { + int preKeyId = message.getPreKeyId(); + PublicKey remoteKey = message.getPublicKey(); + IdentityKey remoteIdentity = message.getIdentityKey(); + + 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)) + throw new InvalidKeyIdException("No such prekey: " + preKeyId); + + PreKeyRecord preKeyRecord = new PreKeyRecord(context, masterSecret, preKeyId); + KeyPair preKeyPair = new KeyPair(preKeyId, preKeyRecord.getKeyPair().getKeyPair(), masterSecret); + + localKeyRecord.setCurrentKeyPair(preKeyPair); + localKeyRecord.setNextKeyPair(preKeyPair); + + remoteKeyRecord.setCurrentRemoteKey(remoteKey); + remoteKeyRecord.setLastRemoteKey(remoteKey); + + sessionRecord.setSessionId(localKeyRecord.getCurrentKeyPair().getPublicKey().getFingerprintBytes(), + remoteKeyRecord.getCurrentRemoteKey().getFingerprintBytes()); + sessionRecord.setIdentityKey(remoteIdentity); + sessionRecord.setSessionVersion(Math.min(message.getSupportedVersion(), EncryptedMessage.SUPPORTED_VERSION)); + + + localKeyRecord.save(); + remoteKeyRecord.save(); + sessionRecord.save(); + + DatabaseFactory.getIdentityDatabase(context) + .saveIdentity(masterSecret, recipient, remoteIdentity); + } + public void processKeyExchangeMessage(PreKeyEntity message) { PublicKey remoteKey = new PublicKey(message.getKeyId(), message.getPublicKey()); remoteKeyRecord.setCurrentRemoteKey(remoteKey); @@ -101,6 +143,8 @@ public class KeyExchangeProcessor { remoteKeyRecord.save(); localKeyRecord = KeyUtil.initializeRecordFor(recipient, context, masterSecret); + localKeyRecord.setNextKeyPair(localKeyRecord.getCurrentKeyPair()); + localKeyRecord.save(); sessionRecord.setSessionId(localKeyRecord.getCurrentKeyPair().getPublicKey().getFingerprintBytes(), remoteKeyRecord.getCurrentRemoteKey().getFingerprintBytes()); diff --git a/src/org/thoughtcrime/securesms/crypto/protocol/KeyExchangeMessage.java b/src/org/thoughtcrime/securesms/crypto/protocol/KeyExchangeMessage.java index a38bde8c3b..6cf784020e 100644 --- a/src/org/thoughtcrime/securesms/crypto/protocol/KeyExchangeMessage.java +++ b/src/org/thoughtcrime/securesms/crypto/protocol/KeyExchangeMessage.java @@ -20,7 +20,7 @@ import android.content.Context; import android.util.Log; import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; -import org.thoughtcrime.securesms.crypto.InvalidVersionException; +import org.whispersystems.textsecure.crypto.InvalidVersionException; import org.whispersystems.textsecure.crypto.IdentityKey; import org.whispersystems.textsecure.crypto.InvalidKeyException; import org.whispersystems.textsecure.crypto.MasterSecret; @@ -96,7 +96,7 @@ public class KeyExchangeMessage { if (messageVersion > EncryptedMessage.SUPPORTED_VERSION) throw new InvalidVersionException("Key exchange with version: " + messageVersion + " but we only support: " + EncryptedMessage.SUPPORTED_VERSION); - + if (messageVersion >= 1) keyBytes = Base64.decodeWithoutPadding(messageBody); diff --git a/src/org/thoughtcrime/securesms/database/MmsSmsColumns.java b/src/org/thoughtcrime/securesms/database/MmsSmsColumns.java index e7d6000017..0579d1dd4e 100644 --- a/src/org/thoughtcrime/securesms/database/MmsSmsColumns.java +++ b/src/org/thoughtcrime/securesms/database/MmsSmsColumns.java @@ -27,9 +27,11 @@ public interface MmsSmsColumns { BASE_SENDING_TYPE, BASE_SENT_FAILED_TYPE}; // Key Exchange Information - protected static final long KEY_EXCHANGE_BIT = 0x8000; - protected static final long KEY_EXCHANGE_STALE_BIT = 0x4000; - protected static final long KEY_EXCHANGE_PROCESSED_BIT = 0x2000; + protected static final long KEY_EXCHANGE_BIT = 0x8000; + protected static final long KEY_EXCHANGE_STALE_BIT = 0x4000; + protected static final long KEY_EXCHANGE_PROCESSED_BIT = 0x2000; + protected static final long KEY_EXCHANGE_CORRUPTED_BIT = 0x1000; + protected static final long KEY_EXCHANGE_INVALID_VERSION_BIT = 0x800; // Secure Message Information protected static final long SECURE_MESSAGE_BIT = 0x800000; @@ -81,6 +83,14 @@ public interface MmsSmsColumns { return (type & KEY_EXCHANGE_PROCESSED_BIT) != 0; } + public static boolean isCorruptedKeyExchange(long type) { + return (type & KEY_EXCHANGE_CORRUPTED_BIT) != 0; + } + + public static boolean isInvalidVersionKeyExchange(long type) { + return (type & KEY_EXCHANGE_INVALID_VERSION_BIT) != 0; + } + public static boolean isSymmetricEncryption(long type) { return (type & ENCRYPTION_SYMMETRIC_BIT) != 0; } diff --git a/src/org/thoughtcrime/securesms/database/SmsDatabase.java b/src/org/thoughtcrime/securesms/database/SmsDatabase.java index 56e2a6298e..703d43ac5b 100644 --- a/src/org/thoughtcrime/securesms/database/SmsDatabase.java +++ b/src/org/thoughtcrime/securesms/database/SmsDatabase.java @@ -235,8 +235,10 @@ public class SmsDatabase extends Database implements MmsSmsColumns { protected Pair insertMessageInbox(IncomingTextMessage message, long type) { if (message.isKeyExchange()) { type |= Types.KEY_EXCHANGE_BIT; - if (((IncomingKeyExchangeMessage)message).isStale()) type |= Types.KEY_EXCHANGE_STALE_BIT; - else if (((IncomingKeyExchangeMessage)message).isProcessed()) type |= Types.KEY_EXCHANGE_PROCESSED_BIT; + if (((IncomingKeyExchangeMessage)message).isStale()) type |= Types.KEY_EXCHANGE_STALE_BIT; + else if (((IncomingKeyExchangeMessage)message).isProcessed()) type |= Types.KEY_EXCHANGE_PROCESSED_BIT; + 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 (message.isSecureMessage()) { type |= Types.SECURE_MESSAGE_BIT; type |= Types.ENCRYPTION_REMOTE_BIT; diff --git a/src/org/thoughtcrime/securesms/database/model/MessageRecord.java b/src/org/thoughtcrime/securesms/database/model/MessageRecord.java index 2f40fe8524..ec43b0843e 100644 --- a/src/org/thoughtcrime/securesms/database/model/MessageRecord.java +++ b/src/org/thoughtcrime/securesms/database/model/MessageRecord.java @@ -103,6 +103,14 @@ public abstract class MessageRecord extends DisplayRecord { return SmsDatabase.Types.isProcessedKeyExchange(type); } + public boolean isCorruptedKeyExchange() { + return SmsDatabase.Types.isCorruptedKeyExchange(type); + } + + public boolean isInvalidVersionKeyExchange() { + return SmsDatabase.Types.isInvalidVersionKeyExchange(type); + } + public Recipient getIndividualRecipient() { return individualRecipient; } diff --git a/src/org/thoughtcrime/securesms/gcm/GcmIntentService.java b/src/org/thoughtcrime/securesms/gcm/GcmIntentService.java index 33f55e7b70..f14cd95848 100644 --- a/src/org/thoughtcrime/securesms/gcm/GcmIntentService.java +++ b/src/org/thoughtcrime/securesms/gcm/GcmIntentService.java @@ -12,7 +12,6 @@ import org.thoughtcrime.securesms.sms.IncomingTextMessage; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.textsecure.push.IncomingPushMessage; import org.whispersystems.textsecure.push.PushServiceSocket; -import org.whispersystems.textsecure.push.RateLimitException; import org.whispersystems.textsecure.util.Util; import java.io.IOException; @@ -73,6 +72,7 @@ public class GcmIntentService extends GCMBaseIntentService { Intent receivedIntent = new Intent(context, SendReceiveService.class); receivedIntent.setAction(SendReceiveService.RECEIVE_SMS_ACTION); receivedIntent.putParcelableArrayListExtra("text_messages", messages); + receivedIntent.putExtra("push_type", message.getType()); context.startService(receivedIntent); } diff --git a/src/org/thoughtcrime/securesms/protocol/WirePrefix.java b/src/org/thoughtcrime/securesms/protocol/WirePrefix.java index 7b4214d358..072232ff4e 100644 --- a/src/org/thoughtcrime/securesms/protocol/WirePrefix.java +++ b/src/org/thoughtcrime/securesms/protocol/WirePrefix.java @@ -47,6 +47,10 @@ public abstract class WirePrefix { return verifyPrefix("?TSM", message); } + public static boolean isPreKeyBundle(String message) { + return verifyPrefix("?TSP", message); + } + public static String calculateKeyExchangePrefix(String message) { return calculatePrefix(("?TSK" + message).getBytes(), PREFIX_BYTES); } @@ -55,6 +59,10 @@ public abstract class WirePrefix { return calculatePrefix(("?TSM" + message).getBytes(), PREFIX_BYTES); } + public static String calculatePreKeyBundlePrefix(String message) { + return calculatePrefix(("?TSP" + message).getBytes(), PREFIX_BYTES); + } + private static boolean verifyPrefix(String prefixType, String message) { if (message.length() <= PREFIX_SIZE) return false; diff --git a/src/org/thoughtcrime/securesms/service/SmsReceiver.java b/src/org/thoughtcrime/securesms/service/SmsReceiver.java index efe8a7604b..11f0dd0d5c 100644 --- a/src/org/thoughtcrime/securesms/service/SmsReceiver.java +++ b/src/org/thoughtcrime/securesms/service/SmsReceiver.java @@ -22,8 +22,11 @@ import android.util.Log; import android.util.Pair; import org.thoughtcrime.securesms.crypto.DecryptingQueue; +import org.thoughtcrime.securesms.sms.IncomingEncryptedMessage; +import org.thoughtcrime.securesms.sms.IncomingPreKeyBundleMessage; +import org.thoughtcrime.securesms.transport.PushTransport; import org.whispersystems.textsecure.crypto.InvalidKeyException; -import org.thoughtcrime.securesms.crypto.InvalidVersionException; +import org.whispersystems.textsecure.crypto.InvalidVersionException; import org.thoughtcrime.securesms.crypto.protocol.KeyExchangeMessage; import org.thoughtcrime.securesms.crypto.KeyExchangeProcessor; import org.whispersystems.textsecure.crypto.MasterSecret; @@ -38,6 +41,8 @@ import org.thoughtcrime.securesms.sms.IncomingKeyExchangeMessage; import org.thoughtcrime.securesms.sms.IncomingTextMessage; import org.thoughtcrime.securesms.sms.MultipartSmsMessageHandler; import org.thoughtcrime.securesms.util.TextSecurePreferences; +import org.whispersystems.textsecure.crypto.protocol.PreKeyBundleMessage; +import org.whispersystems.textsecure.storage.InvalidKeyIdException; import java.util.List; @@ -48,15 +53,32 @@ public class SmsReceiver { private final Context context; public SmsReceiver(Context context) { - this.context = context; + this.context = context; } + private IncomingTextMessage assembleMessageFragments(List messages, int pushType) { + if (messages.size() != 1) return assembleMessageFragments(messages); + + IncomingTextMessage message = messages.get(0); + + switch (pushType) { + case PushTransport.TYPE_MESSAGE_CIPHERTEXT: + return new IncomingEncryptedMessage(message, message.getMessageBody()); + case PushTransport.TYPE_MESSAGE_PREKEY_BUNDLE: + return new IncomingPreKeyBundleMessage(message, message.getMessageBody()); + case PushTransport.TYPE_MESSAGE_KEY_EXCHANGE: + return new IncomingKeyExchangeMessage(message, message.getMessageBody()); + } + + return message; + } private IncomingTextMessage assembleMessageFragments(List messages) { IncomingTextMessage message = new IncomingTextMessage(messages); if (WirePrefix.isEncryptedMessage(message.getMessageBody()) || - WirePrefix.isKeyExchange(message.getMessageBody())) + WirePrefix.isKeyExchange(message.getMessageBody()) || + WirePrefix.isPreKeyBundle(message.getMessageBody())) { return multipartMessageHandler.processPotentialMultipartMessage(message); } else { @@ -91,6 +113,43 @@ public class SmsReceiver { } } + private Pair storePreKeyBundledMessage(MasterSecret masterSecret, + IncomingKeyExchangeMessage message) + { + Log.w("SmsReceiver", "Processing prekey message..."); + + try { + Recipient recipient = new Recipient(null, message.getSender(), null, null); + KeyExchangeProcessor processor = new KeyExchangeProcessor(context, masterSecret, recipient); + PreKeyBundleMessage preKeyExchange = new PreKeyBundleMessage(message.getMessageBody()); + + if (processor.isTrusted(preKeyExchange)) { + processor.processKeyExchangeMessage(preKeyExchange); + + IncomingEncryptedMessage bundledMessage = new IncomingEncryptedMessage(message, preKeyExchange.getBundledMessage()); + Pair messageAndThreadId = storeSecureMessage(masterSecret, bundledMessage); + + Intent intent = new Intent(KeyExchangeProcessor.SECURITY_UPDATE_EVENT); + intent.putExtra("thread_id", messageAndThreadId.second); + intent.setPackage(context.getPackageName()); + context.sendBroadcast(intent, KeyCachingService.KEY_PERMISSION); + + return messageAndThreadId; + } + } catch (InvalidKeyException e) { + Log.w("SmsReceiver", e); + message.setCorrupted(true); + } catch (InvalidVersionException e) { + Log.w("SmsReceiver", e); + message.setInvalidVersion(true); + } catch (InvalidKeyIdException e) { + Log.w("SmsReceiver", e); + message.setStale(true); + } + + return storeStandardMessage(masterSecret, message); + } + private Pair storeKeyExchangeMessage(MasterSecret masterSecret, IncomingKeyExchangeMessage message) { @@ -114,8 +173,10 @@ public class SmsReceiver { } } catch (InvalidVersionException e) { Log.w("SmsReceiver", e); + message.setInvalidVersion(true); } catch (InvalidKeyException e) { Log.w("SmsReceiver", e); + message.setCorrupted(true); } } @@ -124,13 +185,19 @@ public class SmsReceiver { private Pair storeMessage(MasterSecret masterSecret, IncomingTextMessage message) { if (message.isSecureMessage()) return storeSecureMessage(masterSecret, message); - else if (message.isKeyExchange()) return storeKeyExchangeMessage(masterSecret, (IncomingKeyExchangeMessage)message); + else if (message.isPreKeyBundle()) return storePreKeyBundledMessage(masterSecret, (IncomingKeyExchangeMessage) message); + else if (message.isKeyExchange()) return storeKeyExchangeMessage(masterSecret, (IncomingKeyExchangeMessage) message); else return storeStandardMessage(masterSecret, message); } private void handleReceiveMessage(MasterSecret masterSecret, Intent intent) { List messagesList = intent.getExtras().getParcelableArrayList("text_messages"); - IncomingTextMessage message = assembleMessageFragments(messagesList); + int pushType = intent.getIntExtra("push_type", -1); + + IncomingTextMessage message; + + if (pushType != -1) message = assembleMessageFragments(messagesList, pushType); + else message = assembleMessageFragments(messagesList); if (message != null) { Pair messageAndThreadId = storeMessage(masterSecret, message); diff --git a/src/org/thoughtcrime/securesms/sms/IncomingEncryptedMessage.java b/src/org/thoughtcrime/securesms/sms/IncomingEncryptedMessage.java index 8df3ba7d42..63489bf7bc 100644 --- a/src/org/thoughtcrime/securesms/sms/IncomingEncryptedMessage.java +++ b/src/org/thoughtcrime/securesms/sms/IncomingEncryptedMessage.java @@ -2,7 +2,7 @@ package org.thoughtcrime.securesms.sms; public class IncomingEncryptedMessage extends IncomingTextMessage { - IncomingEncryptedMessage(IncomingTextMessage base, String newBody) { + public IncomingEncryptedMessage(IncomingTextMessage base, String newBody) { super(base, newBody); } diff --git a/src/org/thoughtcrime/securesms/sms/IncomingKeyExchangeMessage.java b/src/org/thoughtcrime/securesms/sms/IncomingKeyExchangeMessage.java index 59bfe897ab..56020708bf 100644 --- a/src/org/thoughtcrime/securesms/sms/IncomingKeyExchangeMessage.java +++ b/src/org/thoughtcrime/securesms/sms/IncomingKeyExchangeMessage.java @@ -4,13 +4,17 @@ public class IncomingKeyExchangeMessage extends IncomingTextMessage { private boolean isStale; private boolean isProcessed; + private boolean isCorrupted; + private boolean isInvalidVersion; - IncomingKeyExchangeMessage(IncomingTextMessage base, String newBody) { + public IncomingKeyExchangeMessage(IncomingTextMessage base, String newBody) { super(base, newBody); if (base instanceof IncomingKeyExchangeMessage) { - this.isStale = ((IncomingKeyExchangeMessage)base).isStale; - this.isProcessed = ((IncomingKeyExchangeMessage)base).isProcessed; + this.isStale = ((IncomingKeyExchangeMessage)base).isStale; + this.isProcessed = ((IncomingKeyExchangeMessage)base).isProcessed; + this.isCorrupted = ((IncomingKeyExchangeMessage)base).isCorrupted; + this.isInvalidVersion = ((IncomingKeyExchangeMessage)base).isInvalidVersion; } } @@ -35,6 +39,22 @@ public class IncomingKeyExchangeMessage extends IncomingTextMessage { this.isProcessed = isProcessed; } + public boolean isCorrupted() { + return isCorrupted; + } + + public void setCorrupted(boolean isCorrupted) { + this.isCorrupted = isCorrupted; + } + + public boolean isInvalidVersion() { + return isInvalidVersion; + } + + public void setInvalidVersion(boolean isInvalidVersion) { + this.isInvalidVersion = isInvalidVersion; + } + @Override public boolean isKeyExchange() { return true; diff --git a/src/org/thoughtcrime/securesms/sms/IncomingPreKeyBundleMessage.java b/src/org/thoughtcrime/securesms/sms/IncomingPreKeyBundleMessage.java new file mode 100644 index 0000000000..c4103564b0 --- /dev/null +++ b/src/org/thoughtcrime/securesms/sms/IncomingPreKeyBundleMessage.java @@ -0,0 +1,20 @@ +package org.thoughtcrime.securesms.sms; + +import org.whispersystems.textsecure.push.IncomingPushMessage; + +public class IncomingPreKeyBundleMessage extends IncomingKeyExchangeMessage { + + public IncomingPreKeyBundleMessage(IncomingTextMessage base, String newBody) { + super(base, newBody); + } + + @Override + public IncomingPreKeyBundleMessage withMessageBody(String messageBody) { + return new IncomingPreKeyBundleMessage(this, messageBody); + } + + @Override + public boolean isPreKeyBundle() { + return true; + } +} diff --git a/src/org/thoughtcrime/securesms/sms/IncomingTextMessage.java b/src/org/thoughtcrime/securesms/sms/IncomingTextMessage.java index 4b38ce93ac..fba40c865d 100644 --- a/src/org/thoughtcrime/securesms/sms/IncomingTextMessage.java +++ b/src/org/thoughtcrime/securesms/sms/IncomingTextMessage.java @@ -126,6 +126,10 @@ public class IncomingTextMessage implements Parcelable { return false; } + public boolean isPreKeyBundle() { + return false; + } + @Override public int describeContents() { return 0; diff --git a/src/org/thoughtcrime/securesms/sms/RawTransportDetails.java b/src/org/thoughtcrime/securesms/sms/RawTransportDetails.java new file mode 100644 index 0000000000..17b0fe372a --- /dev/null +++ b/src/org/thoughtcrime/securesms/sms/RawTransportDetails.java @@ -0,0 +1,27 @@ +package org.thoughtcrime.securesms.sms; + +import org.whispersystems.textsecure.crypto.TransportDetails; + +import java.io.IOException; + +public class RawTransportDetails 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 messageWithMac; + } + + @Override + public byte[] getDecodedMessage(byte[] encodedMessageBytes) throws IOException { + return encodedMessageBytes; + } +} diff --git a/src/org/thoughtcrime/securesms/transport/MmsTransport.java b/src/org/thoughtcrime/securesms/transport/MmsTransport.java index 2331c527ab..fd7790e383 100644 --- a/src/org/thoughtcrime/securesms/transport/MmsTransport.java +++ b/src/org/thoughtcrime/securesms/transport/MmsTransport.java @@ -5,6 +5,9 @@ import android.telephony.TelephonyManager; import android.util.Log; import android.util.Pair; +import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; +import org.whispersystems.textsecure.crypto.IdentityKey; +import org.whispersystems.textsecure.crypto.IdentityKeyPair; import org.whispersystems.textsecure.crypto.MasterSecret; import org.whispersystems.textsecure.crypto.SessionCipher; import org.thoughtcrime.securesms.database.MmsDatabase; @@ -134,7 +137,8 @@ public class MmsTransport { } private byte[] getEncryptedPdu(MasterSecret masterSecret, String recipient, byte[] pduBytes) { - EncryptedMessage message = new EncryptedMessage(context, masterSecret, new TextTransport()); + IdentityKeyPair identityKey = IdentityKeyUtil.getIdentityKeyPair(context, masterSecret); + EncryptedMessage message = new EncryptedMessage(context, masterSecret, identityKey, new TextTransport()); return message.encrypt(new Recipient(null, recipient, null, null), pduBytes); } diff --git a/src/org/thoughtcrime/securesms/transport/PushTransport.java b/src/org/thoughtcrime/securesms/transport/PushTransport.java index 16972baa3c..9c27c39689 100644 --- a/src/org/thoughtcrime/securesms/transport/PushTransport.java +++ b/src/org/thoughtcrime/securesms/transport/PushTransport.java @@ -3,16 +3,20 @@ package org.thoughtcrime.securesms.transport; import android.content.Context; import android.util.Log; +import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; import org.thoughtcrime.securesms.crypto.KeyExchangeProcessor; -import org.thoughtcrime.securesms.mms.TextTransport; -import org.thoughtcrime.securesms.recipients.Recipient; -import org.thoughtcrime.securesms.sms.SmsTransportDetails; -import org.whispersystems.textsecure.crypto.MasterSecret; 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.SessionCipher; +import org.whispersystems.textsecure.crypto.IdentityKey; +import org.whispersystems.textsecure.crypto.IdentityKeyPair; +import org.whispersystems.textsecure.crypto.InvalidKeyException; +import org.whispersystems.textsecure.crypto.MasterSecret; +import org.whispersystems.textsecure.crypto.protocol.PreKeyBundleMessage; import org.whispersystems.textsecure.crypto.protocol.EncryptedMessage; import org.whispersystems.textsecure.push.PreKeyEntity; import org.whispersystems.textsecure.push.PushAttachmentData; @@ -31,6 +35,11 @@ import ws.com.google.android.mms.pdu.SendReq; public class PushTransport extends BaseTransport { + public static final int TYPE_MESSAGE_PLAINTEXT = 0; + public static final int TYPE_MESSAGE_CIPHERTEXT = 1; + public static final int TYPE_MESSAGE_KEY_EXCHANGE = 2; + public static final int TYPE_MESSAGE_PREKEY_BUNDLE = 3; + private final Context context; private final MasterSecret masterSecret; @@ -50,18 +59,15 @@ public class PushTransport extends BaseTransport { String recipientCanonicalNumber = PhoneNumberFormatter.formatNumber(recipient.getNumber(), localNumber); -// if (SessionRecord.hasSession(context, recipient)) { -// byte[] cipherText = getEncryptedMessageForExistingSession(recipient, plaintext); -// socket.sendMessage(recipientCanonicalNumber, new String(cipherText)); -// } else { -// byte[] cipherText = getEncryptedMessageForNewSession(socket, recipient, -// recipientCanonicalNumber, -// plaintext); -// socket.sendMessage(recipientCanonicalNumber, new String(cipherText)); -// } - - - socket.sendMessage(recipientCanonicalNumber, message.getBody().getBody()); + 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); + } context.sendBroadcast(constructSentIntent(context, message.getId(), message.getType())); } catch (RateLimitException e) { @@ -78,8 +84,8 @@ public class PushTransport extends BaseTransport { String messageText = PartParser.getMessageText(message.getBody()); List attachments = getAttachmentsFromBody(message.getBody()); - if (attachments.isEmpty()) socket.sendMessage(destinations, messageText); - else socket.sendMessage(destinations, messageText, attachments); + if (attachments.isEmpty()) socket.sendMessage(destinations, messageText, TYPE_MESSAGE_PLAINTEXT); + else socket.sendMessage(destinations, messageText, attachments, TYPE_MESSAGE_PLAINTEXT); } catch (RateLimitException e) { Log.w("PushTransport", e); throw new IOException("Rate limit exceeded."); @@ -103,20 +109,29 @@ public class PushTransport extends BaseTransport { return attachments; } - private byte[] getEncryptedMessageForNewSession(PushServiceSocket socket, Recipient recipient, String canonicalRecipientNumber, String plaintext) throws IOException { - PreKeyEntity preKey = socket.getPreKey(canonicalRecipientNumber); - KeyExchangeProcessor processor = new KeyExchangeProcessor(context, masterSecret, recipient); + private byte[] getEncryptedMessageForNewSession(PushServiceSocket socket, Recipient recipient, + String canonicalRecipientNumber, String plaintext) + throws IOException + { + IdentityKeyPair identityKeyPair = IdentityKeyUtil.getIdentityKeyPair(context, masterSecret); + IdentityKey identityKey = identityKeyPair.getPublicKey(); + PreKeyEntity preKey = socket.getPreKey(canonicalRecipientNumber); + KeyExchangeProcessor processor = new KeyExchangeProcessor(context, masterSecret, recipient); + processor.processKeyExchangeMessage(preKey); - return plaintext.getBytes(); -// synchronized (SessionCipher.CIPHER_LOCK) { -// SessionCipher sessionCipher = new SessionCipher(context, masterSecret, recipient, new SmsTransportDetails()); -// return sessionCipher.encryptMessage(plaintext.getBytes()); -// } + EncryptedMessage message = new EncryptedMessage(context, masterSecret, identityKeyPair, new RawTransportDetails()); + byte[] bundledMessage = message.encrypt(recipient, plaintext.getBytes()); + + PreKeyBundleMessage preKeyBundleMessage = new PreKeyBundleMessage(identityKey, bundledMessage); + return preKeyBundleMessage.serialize().getBytes(); } - private byte[] getEncryptedMessageForExistingSession(Recipient recipient, String plaintext) { - EncryptedMessage message = new EncryptedMessage(context, masterSecret, new TextTransport()); + private byte[] getEncryptedMessageForExistingSession(Recipient recipient, String plaintext) + throws IOException + { + IdentityKeyPair identityKeyPair = IdentityKeyUtil.getIdentityKeyPair(context, masterSecret); + EncryptedMessage message = new EncryptedMessage(context, masterSecret, identityKeyPair, new TextTransport()); return message.encrypt(recipient, plaintext.getBytes()); } diff --git a/src/org/thoughtcrime/securesms/transport/SmsTransport.java b/src/org/thoughtcrime/securesms/transport/SmsTransport.java index 1b000bf3ba..69e245adc1 100644 --- a/src/org/thoughtcrime/securesms/transport/SmsTransport.java +++ b/src/org/thoughtcrime/securesms/transport/SmsTransport.java @@ -5,6 +5,8 @@ import android.content.Context; import android.telephony.SmsManager; import android.util.Log; +import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; +import org.whispersystems.textsecure.crypto.IdentityKeyPair; import org.whispersystems.textsecure.crypto.MasterSecret; import org.whispersystems.textsecure.crypto.SessionCipher; import org.thoughtcrime.securesms.database.model.SmsMessageRecord; @@ -139,7 +141,8 @@ public class SmsTransport extends BaseTransport { } private String getAsymmetricEncrypt(MasterSecret masterSecret, String body, Recipient recipient) { - EncryptedMessage message = new EncryptedMessage(context, masterSecret, new SmsTransportDetails()); + IdentityKeyPair identityKey = IdentityKeyUtil.getIdentityKeyPair(context, masterSecret); + EncryptedMessage message = new EncryptedMessage(context, masterSecret, identityKey, new SmsTransportDetails()); return new String(message.encrypt(recipient, body.getBytes())); } }