Support for Axolotl protocol.

1) Split code into v1 and v2 message paths.

2) Do the Axolotl protocol for v2.

3) Switch all v2 entities to protobuf.
This commit is contained in:
Moxie Marlinspike 2013-11-25 17:00:20 -08:00
parent dc73bc2a5c
commit 44092a3eff
55 changed files with 8774 additions and 1829 deletions

View File

@ -0,0 +1,54 @@
package textsecure;
option java_package = "org.whispersystems.textsecure.storage";
option java_outer_classname = "StorageProtos";
message SessionStructure {
message Chain {
optional bytes senderEphemeral = 1;
optional bytes senderEphemeralPrivate = 2;
message ChainKey {
optional uint32 index = 1;
optional bytes key = 2;
}
optional ChainKey chainKey = 3;
message MessageKey {
optional uint32 index = 1;
optional bytes cipherKey = 2;
optional bytes macKey = 3;
}
repeated MessageKey messageKeys = 4;
}
message PendingKeyExchange {
optional uint32 sequence = 1;
optional bytes localBaseKey = 2;
optional bytes localBaseKeyPrivate = 3;
optional bytes localEphemeralKey = 4;
optional bytes localEphemeralKeyPrivate = 5;
optional bytes localIdentityKey = 7;
optional bytes localIdentityKeyPrivate = 8;
}
message PendingPreKey {
optional uint32 preKeyId = 1;
optional bytes baseKey = 2;
}
optional uint32 sessionVersion = 1;
optional bytes localIdentityPublic = 2;
optional bytes remoteIdentityPublic = 3;
optional bytes rootKey = 4;
optional uint32 previousCounter = 5;
optional Chain senderChain = 6;
repeated Chain receiverChains = 7;
optional PendingKeyExchange pendingKeyExchange = 8;
optional PendingPreKey pendingPreKey = 9;
}

View File

@ -1,3 +1,3 @@
all: all:
protoc --java_out=../src/ IncomingPushMessageSignal.proto protoc --java_out=../src/ IncomingPushMessageSignal.proto WhisperTextProtocol.proto LocalStorageProtocol.proto

View File

@ -0,0 +1,25 @@
package textsecure;
option java_package = "org.whispersystems.textsecure.crypto.protocol";
option java_outer_classname = "WhisperProtos";
message WhisperMessage {
optional bytes ephemeralKey = 1;
optional uint32 counter = 2;
optional uint32 previousCounter = 3;
optional bytes ciphertext = 4;
}
message PreKeyWhisperMessage {
optional uint32 preKeyId = 1;
optional bytes baseKey = 2;
optional bytes identityKey = 3;
optional bytes message = 4; // WhisperMessage
}
message KeyExchangeMessage {
optional uint32 id = 1;
optional bytes baseKey = 2;
optional bytes ephemeralKey = 3;
optional bytes identityKey = 4;
}

View File

@ -47,8 +47,7 @@ public class IdentityKey implements Parcelable, SerializableKey {
} }
}; };
public static final int SIZE = 1 + ECPublicKey.KEY_SIZE; public static final int NIST_SIZE = 1 + ECPublicKey.KEY_SIZE;
private static final int CURRENT_VESION = 1;
private ECPublicKey publicKey; private ECPublicKey publicKey;
@ -73,19 +72,22 @@ public class IdentityKey implements Parcelable, SerializableKey {
} }
private void initializeFromSerialized(byte[] bytes, int offset) throws InvalidKeyException { private void initializeFromSerialized(byte[] bytes, int offset) throws InvalidKeyException {
int version = bytes[offset] & 0xff; if ((bytes[offset] & 0xff) == 1) {
this.publicKey = Curve.decodePoint(bytes, offset +1);
if (version > CURRENT_VESION) } else {
throw new InvalidKeyException("Unsupported key version: " + version); this.publicKey = Curve.decodePoint(bytes, offset);
}
this.publicKey = Curve.decodePoint(bytes, offset + 1);
} }
public byte[] serialize() { public byte[] serialize() {
byte[] versionBytes = {(byte)CURRENT_VESION}; if (publicKey.getType() == Curve.NIST_TYPE) {
byte[] encodedKey = publicKey.serialize(); byte[] versionBytes = {0x01};
byte[] encodedKey = publicKey.serialize();
return Util.combine(versionBytes, encodedKey); return Util.combine(versionBytes, encodedKey);
} else {
return publicKey.serialize();
}
} }
public String getFingerprint() { public String getFingerprint() {

View File

@ -1,92 +0,0 @@
/**
* Copyright (C) 2013 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.crypto;
import android.content.Context;
import android.util.Log;
import org.whispersystems.textsecure.crypto.ecc.Curve;
import org.whispersystems.textsecure.storage.CanonicalRecipientAddress;
import org.whispersystems.textsecure.storage.LocalKeyRecord;
import org.whispersystems.textsecure.storage.RemoteKeyRecord;
import org.whispersystems.textsecure.storage.SessionRecord;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
/**
* Helper class for generating key pairs and calculating ECDH agreements.
*
* @author Moxie Marlinspike
*/
public class KeyUtil {
public static void abortSessionFor(Context context, CanonicalRecipientAddress recipient) {
//XXX Obviously we should probably do something more thorough here eventually.
Log.w("KeyUtil", "Aborting session, deleting keys...");
LocalKeyRecord.delete(context, recipient);
RemoteKeyRecord.delete(context, recipient);
SessionRecord.delete(context, recipient);
}
public static boolean isSessionFor(Context context, CanonicalRecipientAddress recipient) {
Log.w("KeyUtil", "Checking session...");
return
(LocalKeyRecord.hasRecord(context, recipient)) &&
(RemoteKeyRecord.hasRecord(context, recipient)) &&
(SessionRecord.hasSession(context, recipient));
}
public static boolean isNonPrekeySessionFor(Context context, MasterSecret masterSecret, CanonicalRecipientAddress recipient) {
return isSessionFor(context, recipient) &&
!(new SessionRecord(context, masterSecret, recipient).isPrekeyBundleRequired());
}
public static boolean isIdentityKeyFor(Context context,
MasterSecret masterSecret,
CanonicalRecipientAddress recipient)
{
return isSessionFor(context, recipient) &&
new SessionRecord(context, masterSecret, recipient).getIdentityKey() != null;
}
public static LocalKeyRecord initializeRecordFor(Context context,
MasterSecret masterSecret,
CanonicalRecipientAddress recipient,
int sessionVersion)
{
Log.w("KeyUtil", "Initializing local key pairs...");
try {
SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
int initialId = secureRandom.nextInt(4094) + 1;
KeyPair currentPair = new KeyPair(initialId, Curve.generateKeyPairForSession(sessionVersion), masterSecret);
KeyPair nextPair = new KeyPair(initialId + 1, Curve.generateKeyPairForSession(sessionVersion), masterSecret);
LocalKeyRecord record = new LocalKeyRecord(context, masterSecret, recipient);
record.setCurrentKeyPair(currentPair);
record.setNextKeyPair(nextPair);
record.save();
return record;
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
}

View File

@ -1,85 +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.whispersystems.textsecure.crypto;
import android.content.Context;
import org.whispersystems.textsecure.crypto.SessionCipher.SessionCipherContext;
import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage;
import org.whispersystems.textsecure.storage.CanonicalRecipientAddress;
/**
* Parses and serializes the encrypted message format.
*
* @author Moxie Marlinspike
*/
public class MessageCipher {
private final Context context;
private final MasterSecret masterSecret;
private final IdentityKeyPair localIdentityKey;
public MessageCipher(Context context, MasterSecret masterSecret, IdentityKeyPair localIdentityKey) {
this.context = context.getApplicationContext();
this.masterSecret = masterSecret;
this.localIdentityKey = localIdentityKey;
}
public CiphertextMessage encrypt(CanonicalRecipientAddress recipient, byte[] paddedBody) {
synchronized (SessionCipher.CIPHER_LOCK) {
SessionCipher sessionCipher = new SessionCipher();
SessionCipherContext sessionContext = sessionCipher.getEncryptionContext(context, masterSecret, localIdentityKey, recipient);
byte[] ciphertextBody = sessionCipher.encrypt(sessionContext, paddedBody);
return new CiphertextMessage(sessionContext, ciphertextBody);
}
}
public byte[] decrypt(CanonicalRecipientAddress recipient, byte[] ciphertext)
throws InvalidMessageException
{
synchronized (SessionCipher.CIPHER_LOCK) {
try {
CiphertextMessage message = new CiphertextMessage(ciphertext);
int messageVersion = message.getCurrentVersion();
int senderKeyId = message.getSenderKeyId();
int receiverKeyId = message.getReceiverKeyId();
PublicKey nextRemoteKey = new PublicKey(message.getNextKeyBytes());
int counter = message.getCounter();
byte[] body = message.getBody();
SessionCipher sessionCipher = new SessionCipher();
SessionCipherContext sessionContext = sessionCipher.getDecryptionContext(context, masterSecret,
localIdentityKey,
recipient, senderKeyId,
receiverKeyId,
nextRemoteKey,
counter,
messageVersion);
message.verifyMac(sessionContext);
return sessionCipher.decrypt(sessionContext, body);
} catch (InvalidKeyException e) {
throw new InvalidMessageException(e);
}
}
}
}

View File

@ -1,68 +0,0 @@
/**
* Copyright (C) 2011 Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.crypto;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import org.whispersystems.textsecure.util.Hex;
import android.util.Log;
public class MessageMac {
public static final int MAC_LENGTH = 10;
public static byte[] calculateMac(byte[] message, int offset, int length, SecretKeySpec macKey) {
try {
Mac mac = Mac.getInstance("HmacSHA1");
mac.init(macKey);
assert(mac.getMacLength() >= MAC_LENGTH);
mac.update(message, offset, length);
byte[] macBytes = mac.doFinal();
byte[] truncatedMacBytes = new byte[MAC_LENGTH];
System.arraycopy(macBytes, 0, truncatedMacBytes, 0, truncatedMacBytes.length);
return truncatedMacBytes;
} catch (NoSuchAlgorithmException e) {
throw new IllegalArgumentException(e);
} catch (InvalidKeyException e) {
throw new IllegalArgumentException(e);
}
}
public static void verifyMac(byte[] message, int offset, int length,
byte[] receivedMac, SecretKeySpec macKey)
throws InvalidMacException
{
byte[] localMac = calculateMac(message, offset, length, macKey);
Log.w("MessageMac", "Local Mac: " + Hex.toString(localMac));
Log.w("MessageMac", "Remot Mac: " + Hex.toString(receivedMac));
if (!Arrays.equals(localMac, receivedMac)) {
throw new InvalidMacException("MAC on message does not match calculated MAC.");
}
}
}

View File

@ -16,345 +16,31 @@
*/ */
package org.whispersystems.textsecure.crypto; package org.whispersystems.textsecure.crypto;
import android.content.Context;
import android.util.Log;
import org.whispersystems.textsecure.crypto.ecc.ECPublicKey; import android.content.Context;
import org.whispersystems.textsecure.crypto.kdf.DerivedSecrets;
import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage; import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage;
import org.whispersystems.textsecure.storage.CanonicalRecipientAddress; import org.whispersystems.textsecure.storage.CanonicalRecipientAddress;
import org.whispersystems.textsecure.storage.InvalidKeyIdException; import org.whispersystems.textsecure.storage.SessionRecordV1;
import org.whispersystems.textsecure.storage.LocalKeyRecord; import org.whispersystems.textsecure.storage.SessionRecordV2;
import org.whispersystems.textsecure.storage.RemoteKeyRecord;
import org.whispersystems.textsecure.storage.SessionKey;
import org.whispersystems.textsecure.storage.SessionRecord;
import org.whispersystems.textsecure.util.Conversions;
import java.security.InvalidAlgorithmParameterException; public abstract class SessionCipher {
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import javax.crypto.BadPaddingException; protected static final Object SESSION_LOCK = new Object();
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
/** public abstract CiphertextMessage encrypt(byte[] paddedMessage);
* This is where the session encryption magic happens. Implements a compressed version of the OTR protocol. public abstract byte[] decrypt(byte[] decodedMessage) throws InvalidMessageException;
*
* @author Moxie Marlinspike
*/
public class SessionCipher { public static SessionCipher createFor(Context context, MasterSecret masterSecret,
CanonicalRecipientAddress recipient)
public static final Object CIPHER_LOCK = new Object();
public static final int CIPHER_KEY_LENGTH = 16;
public static final int MAC_KEY_LENGTH = 20;
public SessionCipherContext getEncryptionContext(Context context,
MasterSecret masterSecret,
IdentityKeyPair localIdentityKey,
CanonicalRecipientAddress recipient)
{ {
try { if (SessionRecordV2.hasSession(context, masterSecret, recipient)) {
KeyRecords records = getKeyRecords(context, masterSecret, recipient); return new SessionCipherV2(context, masterSecret, recipient);
int localKeyId = records.getLocalKeyRecord().getCurrentKeyPair().getId(); } else if (SessionRecordV1.hasSession(context, recipient)) {
int remoteKeyId = records.getRemoteKeyRecord().getCurrentRemoteKey().getId(); return new SessionCipherV1(context, masterSecret, recipient);
int sessionVersion = records.getSessionRecord().getSessionVersion();
SessionKey sessionKey = getSessionKey(masterSecret, Cipher.ENCRYPT_MODE, sessionVersion, localIdentityKey, records, localKeyId, remoteKeyId);
PublicKey nextKey = records.getLocalKeyRecord().getNextKeyPair().getPublicKey();
int counter = records.getSessionRecord().getCounter();
return new SessionCipherContext(records, sessionKey, localKeyId, remoteKeyId,
nextKey, counter, sessionVersion);
} catch (InvalidKeyIdException e) {
throw new IllegalArgumentException(e);
} catch (InvalidKeyException 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 messageVersion)
throws InvalidMessageException
{
try {
KeyRecords records = getKeyRecords(context, masterSecret, recipient);
if (messageVersion < records.getSessionRecord().getNegotiatedSessionVersion()) {
throw new InvalidMessageException("Message version: " + messageVersion +
" but negotiated session version: " +
records.getSessionRecord().getNegotiatedSessionVersion());
}
SessionKey sessionKey = getSessionKey(masterSecret, Cipher.DECRYPT_MODE, messageVersion,
localIdentityKey, records, recipientKeyId, senderKeyId);
return new SessionCipherContext(records, sessionKey, senderKeyId,
recipientKeyId, nextKey, counter,
messageVersion);
} catch (InvalidKeyIdException e) {
throw new InvalidMessageException(e);
} catch (InvalidKeyException e) {
throw new InvalidMessageException(e);
}
}
public byte[] encrypt(SessionCipherContext context, byte[] paddedMessageBody) {
Log.w("SessionCipher", "Encrypting message...");
try {
byte[]cipherText = getCiphertext(paddedMessageBody, context.getSessionKey().getCipherKey(), context.getSessionRecord().getCounter());
context.getSessionRecord().setSessionKey(context.getSessionKey());
context.getSessionRecord().incrementCounter();
context.getSessionRecord().save();
return cipherText;
} catch (IllegalBlockSizeException e) {
throw new IllegalArgumentException(e);
} catch (BadPaddingException e) {
throw new IllegalArgumentException(e);
}
}
public byte[] decrypt(SessionCipherContext context, byte[] decodedCiphertext)
throws InvalidMessageException
{
Log.w("SessionCipher", "Decrypting message...");
try {
byte[] plaintextWithPadding = getPlaintext(decodedCiphertext,
context.getSessionKey().getCipherKey(),
context.getCounter());
context.getRemoteKeyRecord().updateCurrentRemoteKey(context.getNextKey());
context.getRemoteKeyRecord().save();
context.getLocalKeyRecord().advanceKeyIfNecessary(context.getRecipientKeyId());
context.getLocalKeyRecord().save();
context.getSessionRecord().setSessionKey(context.getSessionKey());
context.getSessionRecord().setPrekeyBundleRequired(false);
context.getSessionRecord().save();
return plaintextWithPadding;
} catch (IllegalBlockSizeException e) {
throw new InvalidMessageException("assert", e);
} catch (BadPaddingException e) {
throw new InvalidMessageException("assert", e);
}
}
private byte[] getPlaintext(byte[] cipherText, SecretKeySpec key, int counter)
throws IllegalBlockSizeException, BadPaddingException
{
Cipher cipher = getCipher(Cipher.DECRYPT_MODE, key, counter);
return cipher.doFinal(cipherText);
}
private byte[] getCiphertext(byte[] message, SecretKeySpec key, int counter)
throws IllegalBlockSizeException, BadPaddingException
{
Cipher cipher = getCipher(Cipher.ENCRYPT_MODE, key, counter);
return cipher.doFinal(message);
}
private Cipher getCipher(int mode, SecretKeySpec key, int counter) {
try {
Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
byte[] ivBytes = new byte[16];
Conversions.mediumToByteArray(ivBytes, 0, counter);
IvParameterSpec iv = new IvParameterSpec(ivBytes);
cipher.init(mode, key, iv);
return cipher;
} catch (NoSuchAlgorithmException e) {
throw new IllegalArgumentException("AES Not Supported!");
} catch (NoSuchPaddingException e) {
throw new IllegalArgumentException("NoPadding Not Supported!");
} catch (java.security.InvalidKeyException e) {
Log.w("SessionCipher", e);
throw new IllegalArgumentException("Invaid Key?");
} catch (InvalidAlgorithmParameterException e) {
Log.w("SessionCipher", e);
throw new IllegalArgumentException("Bad IV?");
}
}
private SessionKey getSessionKey(MasterSecret masterSecret, int mode,
int messageVersion,
IdentityKeyPair localIdentityKey,
KeyRecords records,
int localKeyId, int remoteKeyId)
throws InvalidKeyIdException, InvalidKeyException
{
Log.w("SessionCipher", "Getting session key for local: " + localKeyId + " remote: " + remoteKeyId);
SessionKey sessionKey = records.getSessionRecord().getSessionKey(mode, localKeyId, remoteKeyId);
if (sessionKey != null)
return sessionKey;
DerivedSecrets derivedSecrets = calculateSharedSecret(messageVersion, mode, localIdentityKey,
records, localKeyId, remoteKeyId);
return new SessionKey(mode, localKeyId, remoteKeyId, derivedSecrets.getCipherKey(),
derivedSecrets.getMacKey(), masterSecret);
}
private DerivedSecrets calculateSharedSecret(int messageVersion, int mode,
IdentityKeyPair localIdentityKey,
KeyRecords records,
int localKeyId, int remoteKeyId)
throws InvalidKeyIdException, InvalidKeyException
{
KeyPair localKeyPair = records.getLocalKeyRecord().getKeyPairForId(localKeyId);
ECPublicKey remoteKey = records.getRemoteKeyRecord().getKeyForId(remoteKeyId).getKey();
IdentityKey remoteIdentityKey = records.getSessionRecord().getIdentityKey();
boolean isLowEnd = isLowEnd(records, localKeyId, remoteKeyId);
isLowEnd = (mode == Cipher.ENCRYPT_MODE ? isLowEnd : !isLowEnd);
if (isInitiallyExchangedKeys(records, localKeyId, remoteKeyId) &&
messageVersion >= CiphertextMessage.DHE3_INTRODUCED_VERSION)
{
return SharedSecretCalculator.calculateSharedSecret(isLowEnd,
localKeyPair, localKeyId, localIdentityKey,
remoteKey, remoteKeyId, remoteIdentityKey);
} else { } else {
return SharedSecretCalculator.calculateSharedSecret(messageVersion, isLowEnd, throw new AssertionError("Attempt to initialize cipher for non-existing session.");
localKeyPair, localKeyId,
remoteKey, remoteKeyId);
} }
} }
private boolean isLowEnd(KeyRecords records, int localKeyId, int remoteKeyId) }
throws InvalidKeyIdException
{
ECPublicKey localPublic = records.getLocalKeyRecord().getKeyPairForId(localKeyId).getPublicKey().getKey();
ECPublicKey remotePublic = records.getRemoteKeyRecord().getKeyForId(remoteKeyId).getKey();
return localPublic.compareTo(remotePublic) < 0;
}
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,
CanonicalRecipientAddress recipient)
{
LocalKeyRecord localKeyRecord = new LocalKeyRecord(context, masterSecret, recipient);
RemoteKeyRecord remoteKeyRecord = new RemoteKeyRecord(context, recipient);
SessionRecord sessionRecord = new SessionRecord(context, masterSecret, recipient);
return new KeyRecords(localKeyRecord, remoteKeyRecord, sessionRecord);
}
private static class KeyRecords {
private final LocalKeyRecord localKeyRecord;
private final RemoteKeyRecord remoteKeyRecord;
private final SessionRecord sessionRecord;
public KeyRecords(LocalKeyRecord localKeyRecord, RemoteKeyRecord remoteKeyRecord, SessionRecord sessionRecord) {
this.localKeyRecord = localKeyRecord;
this.remoteKeyRecord = remoteKeyRecord;
this.sessionRecord = sessionRecord;
}
private LocalKeyRecord getLocalKeyRecord() {
return localKeyRecord;
}
private RemoteKeyRecord getRemoteKeyRecord() {
return remoteKeyRecord;
}
private SessionRecord getSessionRecord() {
return sessionRecord;
}
}
public static class SessionCipherContext {
private final LocalKeyRecord localKeyRecord;
private final RemoteKeyRecord remoteKeyRecord;
private final SessionRecord sessionRecord;
private final SessionKey sessionKey;
private final int senderKeyId;
private final int recipientKeyId;
private final PublicKey nextKey;
private final int counter;
private final int messageVersion;
public SessionCipherContext(KeyRecords records,
SessionKey sessionKey,
int senderKeyId,
int receiverKeyId,
PublicKey nextKey,
int counter,
int messageVersion)
{
this.localKeyRecord = records.getLocalKeyRecord();
this.remoteKeyRecord = records.getRemoteKeyRecord();
this.sessionRecord = records.getSessionRecord();
this.sessionKey = sessionKey;
this.senderKeyId = senderKeyId;
this.recipientKeyId = receiverKeyId;
this.nextKey = nextKey;
this.counter = counter;
this.messageVersion = messageVersion;
}
public LocalKeyRecord getLocalKeyRecord() {
return localKeyRecord;
}
public RemoteKeyRecord getRemoteKeyRecord() {
return remoteKeyRecord;
}
public SessionRecord getSessionRecord() {
return sessionRecord;
}
public SessionKey getSessionKey() {
return sessionKey;
}
public PublicKey getNextKey() {
return nextKey;
}
public int getCounter() {
return counter;
}
public int getSenderKeyId() {
return senderKeyId;
}
public int getRecipientKeyId() {
return recipientKeyId;
}
public int getMessageVersion() {
return messageVersion;
}
}
}

View File

@ -0,0 +1,325 @@
package org.whispersystems.textsecure.crypto;
import android.content.Context;
import android.util.Log;
import org.whispersystems.textsecure.crypto.ecc.Curve;
import org.whispersystems.textsecure.crypto.ecc.ECPublicKey;
import org.whispersystems.textsecure.crypto.kdf.DerivedSecrets;
import org.whispersystems.textsecure.crypto.kdf.NKDF;
import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage;
import org.whispersystems.textsecure.crypto.protocol.WhisperMessageV1;
import org.whispersystems.textsecure.storage.CanonicalRecipientAddress;
import org.whispersystems.textsecure.storage.InvalidKeyIdException;
import org.whispersystems.textsecure.storage.LocalKeyRecord;
import org.whispersystems.textsecure.storage.RemoteKeyRecord;
import org.whispersystems.textsecure.storage.SessionKey;
import org.whispersystems.textsecure.storage.SessionRecordV1;
import org.whispersystems.textsecure.util.Conversions;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class SessionCipherV1 extends SessionCipher {
private final Context context;
private final MasterSecret masterSecret;
private final CanonicalRecipientAddress recipient;
public SessionCipherV1(Context context, MasterSecret masterSecret,
CanonicalRecipientAddress recipient)
{
this.context = context;
this.masterSecret = masterSecret;
this.recipient = recipient;
}
public CiphertextMessage encrypt(byte[] paddedMessageBody) {
synchronized (SESSION_LOCK) {
SessionCipherContext encryptionContext = getEncryptionContext();
byte[] cipherText = getCiphertext(paddedMessageBody,
encryptionContext.getSessionKey().getCipherKey(),
encryptionContext.getSessionRecord().getCounter());
encryptionContext.getSessionRecord().setSessionKey(encryptionContext.getSessionKey());
encryptionContext.getSessionRecord().incrementCounter();
encryptionContext.getSessionRecord().save();
return new WhisperMessageV1(encryptionContext, cipherText);
}
}
public byte[] decrypt(byte[] decodedCiphertext) throws InvalidMessageException {
synchronized (SESSION_LOCK) {
WhisperMessageV1 message = new WhisperMessageV1(decodedCiphertext);
SessionCipherContext decryptionContext = getDecryptionContext(message);
message.verifyMac(decryptionContext);
byte[] plaintextWithPadding = getPlaintext(message.getBody(),
decryptionContext.getSessionKey().getCipherKey(),
decryptionContext.getCounter());
decryptionContext.getRemoteKeyRecord().updateCurrentRemoteKey(decryptionContext.getNextKey());
decryptionContext.getRemoteKeyRecord().save();
decryptionContext.getLocalKeyRecord().advanceKeyIfNecessary(decryptionContext.getRecipientKeyId());
decryptionContext.getLocalKeyRecord().save();
decryptionContext.getSessionRecord().setSessionKey(decryptionContext.getSessionKey());
decryptionContext.getSessionRecord().save();
return plaintextWithPadding;
}
}
private SessionCipherContext getEncryptionContext() {
try {
KeyRecords records = getKeyRecords(context, masterSecret, recipient);
int localKeyId = records.getLocalKeyRecord().getCurrentKeyPair().getId();
int remoteKeyId = records.getRemoteKeyRecord().getCurrentRemoteKey().getId();
int sessionVersion = records.getSessionRecord().getSessionVersion();
SessionKey sessionKey = getSessionKey(masterSecret, Cipher.ENCRYPT_MODE,
records, localKeyId, remoteKeyId);
PublicKey nextKey = records.getLocalKeyRecord().getNextKeyPair().getPublicKey();
int counter = records.getSessionRecord().getCounter();
return new SessionCipherContext(records, sessionKey, localKeyId, remoteKeyId,
nextKey, counter, sessionVersion);
} catch (InvalidKeyIdException e) {
throw new IllegalArgumentException(e);
} catch (InvalidKeyException e) {
throw new IllegalArgumentException(e);
}
}
public SessionCipherContext getDecryptionContext(WhisperMessageV1 message)
throws InvalidMessageException
{
try {
KeyRecords records = getKeyRecords(context, masterSecret, recipient);
int messageVersion = message.getCurrentVersion();
int recipientKeyId = message.getReceiverKeyId();
int senderKeyId = message.getSenderKeyId();
PublicKey nextKey = new PublicKey(message.getNextKeyBytes());
int counter = message.getCounter();
if (messageVersion < records.getSessionRecord().getSessionVersion()) {
throw new InvalidMessageException("Message version: " + messageVersion +
" but negotiated session version: " +
records.getSessionRecord().getSessionVersion());
}
SessionKey sessionKey = getSessionKey(masterSecret, Cipher.DECRYPT_MODE,
records, recipientKeyId, senderKeyId);
return new SessionCipherContext(records, sessionKey, senderKeyId,
recipientKeyId, nextKey, counter,
messageVersion);
} catch (InvalidKeyIdException e) {
throw new InvalidMessageException(e);
} catch (InvalidKeyException e) {
throw new InvalidMessageException(e);
}
}
private byte[] getCiphertext(byte[] message, SecretKeySpec key, int counter) {
try {
Cipher cipher = getCipher(Cipher.ENCRYPT_MODE, key, counter);
return cipher.doFinal(message);
} catch (IllegalBlockSizeException e) {
throw new AssertionError(e);
} catch (BadPaddingException e) {
throw new AssertionError(e);
}
}
private byte[] getPlaintext(byte[] cipherText, SecretKeySpec key, int counter) {
try {
Cipher cipher = getCipher(Cipher.DECRYPT_MODE, key, counter);
return cipher.doFinal(cipherText);
} catch (IllegalBlockSizeException e) {
throw new AssertionError(e);
} catch (BadPaddingException e) {
throw new AssertionError(e);
}
}
private Cipher getCipher(int mode, SecretKeySpec key, int counter) {
try {
Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
byte[] ivBytes = new byte[16];
Conversions.mediumToByteArray(ivBytes, 0, counter);
IvParameterSpec iv = new IvParameterSpec(ivBytes);
cipher.init(mode, key, iv);
return cipher;
} catch (NoSuchAlgorithmException e) {
throw new IllegalArgumentException("AES Not Supported!");
} catch (NoSuchPaddingException e) {
throw new IllegalArgumentException("NoPadding Not Supported!");
} catch (java.security.InvalidKeyException e) {
Log.w("SessionCipher", e);
throw new IllegalArgumentException("Invaid Key?");
} catch (InvalidAlgorithmParameterException e) {
Log.w("SessionCipher", e);
throw new IllegalArgumentException("Bad IV?");
}
}
private SessionKey getSessionKey(MasterSecret masterSecret, int mode,
KeyRecords records,
int localKeyId, int remoteKeyId)
throws InvalidKeyIdException, InvalidKeyException
{
Log.w("SessionCipher", "Getting session key for local: " + localKeyId + " remote: " + remoteKeyId);
SessionKey sessionKey = records.getSessionRecord().getSessionKey(mode, localKeyId, remoteKeyId);
if (sessionKey != null)
return sessionKey;
DerivedSecrets derivedSecrets = calculateSharedSecret(mode, records, localKeyId, remoteKeyId);
return new SessionKey(mode, localKeyId, remoteKeyId, derivedSecrets.getCipherKey(),
derivedSecrets.getMacKey(), masterSecret);
}
private DerivedSecrets calculateSharedSecret(int mode, KeyRecords records,
int localKeyId, int remoteKeyId)
throws InvalidKeyIdException, InvalidKeyException
{
NKDF kdf = new NKDF();
KeyPair localKeyPair = records.getLocalKeyRecord().getKeyPairForId(localKeyId);
ECPublicKey remoteKey = records.getRemoteKeyRecord().getKeyForId(remoteKeyId).getKey();
byte[] sharedSecret = Curve.calculateAgreement(remoteKey, localKeyPair.getPrivateKey());
boolean isLowEnd = isLowEnd(records, localKeyId, remoteKeyId);
isLowEnd = (mode == Cipher.ENCRYPT_MODE ? isLowEnd : !isLowEnd);
return kdf.deriveSecrets(sharedSecret, isLowEnd);
}
private boolean isLowEnd(KeyRecords records, int localKeyId, int remoteKeyId)
throws InvalidKeyIdException
{
ECPublicKey localPublic = records.getLocalKeyRecord().getKeyPairForId(localKeyId).getPublicKey().getKey();
ECPublicKey remotePublic = records.getRemoteKeyRecord().getKeyForId(remoteKeyId).getKey();
return localPublic.compareTo(remotePublic) < 0;
}
private KeyRecords getKeyRecords(Context context, MasterSecret masterSecret,
CanonicalRecipientAddress recipient)
{
LocalKeyRecord localKeyRecord = new LocalKeyRecord(context, masterSecret, recipient);
RemoteKeyRecord remoteKeyRecord = new RemoteKeyRecord(context, recipient);
SessionRecordV1 sessionRecord = new SessionRecordV1(context, masterSecret, recipient);
return new KeyRecords(localKeyRecord, remoteKeyRecord, sessionRecord);
}
private static class KeyRecords {
private final LocalKeyRecord localKeyRecord;
private final RemoteKeyRecord remoteKeyRecord;
private final SessionRecordV1 sessionRecord;
public KeyRecords(LocalKeyRecord localKeyRecord,
RemoteKeyRecord remoteKeyRecord,
SessionRecordV1 sessionRecord)
{
this.localKeyRecord = localKeyRecord;
this.remoteKeyRecord = remoteKeyRecord;
this.sessionRecord = sessionRecord;
}
private LocalKeyRecord getLocalKeyRecord() {
return localKeyRecord;
}
private RemoteKeyRecord getRemoteKeyRecord() {
return remoteKeyRecord;
}
private SessionRecordV1 getSessionRecord() {
return sessionRecord;
}
}
public static class SessionCipherContext {
private final LocalKeyRecord localKeyRecord;
private final RemoteKeyRecord remoteKeyRecord;
private final SessionRecordV1 sessionRecord;
private final SessionKey sessionKey;
private final int senderKeyId;
private final int recipientKeyId;
private final PublicKey nextKey;
private final int counter;
private final int messageVersion;
public SessionCipherContext(KeyRecords records,
SessionKey sessionKey,
int senderKeyId,
int receiverKeyId,
PublicKey nextKey,
int counter,
int messageVersion)
{
this.localKeyRecord = records.getLocalKeyRecord();
this.remoteKeyRecord = records.getRemoteKeyRecord();
this.sessionRecord = records.getSessionRecord();
this.sessionKey = sessionKey;
this.senderKeyId = senderKeyId;
this.recipientKeyId = receiverKeyId;
this.nextKey = nextKey;
this.counter = counter;
this.messageVersion = messageVersion;
}
public LocalKeyRecord getLocalKeyRecord() {
return localKeyRecord;
}
public RemoteKeyRecord getRemoteKeyRecord() {
return remoteKeyRecord;
}
public SessionRecordV1 getSessionRecord() {
return sessionRecord;
}
public SessionKey getSessionKey() {
return sessionKey;
}
public PublicKey getNextKey() {
return nextKey;
}
public int getCounter() {
return counter;
}
public int getSenderKeyId() {
return senderKeyId;
}
public int getRecipientKeyId() {
return recipientKeyId;
}
public int getMessageVersion() {
return messageVersion;
}
}
}

View File

@ -0,0 +1,201 @@
package org.whispersystems.textsecure.crypto;
import android.content.Context;
import android.util.Log;
import android.util.Pair;
import org.whispersystems.textsecure.crypto.ecc.Curve;
import org.whispersystems.textsecure.crypto.ecc.ECKeyPair;
import org.whispersystems.textsecure.crypto.ecc.ECPublicKey;
import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage;
import org.whispersystems.textsecure.crypto.protocol.PreKeyWhisperMessage;
import org.whispersystems.textsecure.crypto.protocol.WhisperMessageV2;
import org.whispersystems.textsecure.crypto.ratchet.ChainKey;
import org.whispersystems.textsecure.crypto.ratchet.MessageKeys;
import org.whispersystems.textsecure.crypto.ratchet.RootKey;
import org.whispersystems.textsecure.storage.CanonicalRecipientAddress;
import org.whispersystems.textsecure.storage.SessionRecordV2;
import org.whispersystems.textsecure.util.Conversions;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class SessionCipherV2 extends SessionCipher {
private final Context context;
private final MasterSecret masterSecret;
private final CanonicalRecipientAddress recipient;
public SessionCipherV2(Context context,
MasterSecret masterSecret,
CanonicalRecipientAddress recipient)
{
this.context = context;
this.masterSecret = masterSecret;
this.recipient = recipient;
}
@Override
public CiphertextMessage encrypt(byte[] paddedMessage) {
synchronized (SESSION_LOCK) {
SessionRecordV2 sessionRecord = getSessionRecord();
ChainKey chainKey = sessionRecord.getSenderChainKey();
MessageKeys messageKeys = chainKey.getMessageKeys();
ECPublicKey senderEphemeral = sessionRecord.getSenderEphemeral();
int previousCounter = sessionRecord.getPreviousCounter();
byte[] ciphertextBody = getCiphertext(messageKeys, paddedMessage);
CiphertextMessage ciphertextMessage = new WhisperMessageV2(messageKeys.getMacKey(),
senderEphemeral, chainKey.getIndex(),
previousCounter, ciphertextBody);
if (sessionRecord.hasPendingPreKey()) {
Pair<Integer, ECPublicKey> pendingPreKey = sessionRecord.getPendingPreKey();
ciphertextMessage = new PreKeyWhisperMessage(pendingPreKey.first, pendingPreKey.second,
sessionRecord.getLocalIdentityKey(),
(WhisperMessageV2) ciphertextMessage);
}
sessionRecord.setSenderChainKey(chainKey.getNextChainKey());
sessionRecord.save();
return ciphertextMessage;
}
}
@Override
public byte[] decrypt(byte[] decodedMessage) throws InvalidMessageException {
synchronized (SESSION_LOCK) {
SessionRecordV2 sessionRecord = getSessionRecord();
WhisperMessageV2 ciphertextMessage = new WhisperMessageV2(decodedMessage);
ECPublicKey theirEphemeral = ciphertextMessage.getSenderEphemeral();
int counter = ciphertextMessage.getCounter();
ChainKey chainKey = getOrCreateChainKey(sessionRecord, theirEphemeral);
MessageKeys messageKeys = getOrCreateMessageKeys(sessionRecord, theirEphemeral,
chainKey, counter);
ciphertextMessage.verifyMac(messageKeys.getMacKey());
byte[] plaintext = getPlaintext(messageKeys, ciphertextMessage.getBody());
sessionRecord.clearPendingPreKey();
sessionRecord.save();
return plaintext;
}
}
private ChainKey getOrCreateChainKey(SessionRecordV2 sessionRecord, ECPublicKey theirEphemeral)
throws InvalidMessageException
{
try {
if (sessionRecord.hasReceiverChain(theirEphemeral)) {
return sessionRecord.getReceiverChainKey(theirEphemeral);
} else {
RootKey rootKey = sessionRecord.getRootKey();
ECKeyPair ourEphemeral = sessionRecord.getSenderEphemeralPair();
Pair<RootKey, ChainKey> receiverChain = rootKey.createChain(theirEphemeral, ourEphemeral);
ECKeyPair ourNewEphemeral = Curve.generateKeyPairForType(Curve.DJB_TYPE);
Pair<RootKey, ChainKey> senderChain = receiverChain.first.createChain(theirEphemeral, ourNewEphemeral);
sessionRecord.setRootKey(senderChain.first);
sessionRecord.addReceiverChain(theirEphemeral, receiverChain.second);
sessionRecord.setPreviousCounter(sessionRecord.getSenderChainKey().getIndex()-1);
sessionRecord.setSenderChain(ourNewEphemeral, senderChain.second);
return receiverChain.second;
}
} catch (InvalidKeyException e) {
throw new InvalidMessageException(e);
}
}
private MessageKeys getOrCreateMessageKeys(SessionRecordV2 sessionRecord,
ECPublicKey theirEphemeral,
ChainKey chainKey, int counter)
throws InvalidMessageException
{
if (chainKey.getIndex() > counter) {
if (sessionRecord.hasMessageKeys(theirEphemeral, counter)) {
return sessionRecord.removeMessageKeys(theirEphemeral, counter);
} else {
throw new InvalidMessageException("Received message with old counter!");
}
}
if (chainKey.getIndex() - counter > 500) {
throw new InvalidMessageException("Over 500 messages into the future!");
}
while (chainKey.getIndex() < counter) {
MessageKeys messageKeys = chainKey.getMessageKeys();
sessionRecord.setMessageKeys(theirEphemeral, messageKeys);
chainKey = chainKey.getNextChainKey();
}
sessionRecord.setReceiverChainKey(theirEphemeral, chainKey.getNextChainKey());
return chainKey.getMessageKeys();
}
private byte[] getCiphertext(MessageKeys messageKeys, byte[] plaintext) {
try {
Cipher cipher = getCipher(Cipher.ENCRYPT_MODE,
messageKeys.getCipherKey(),
messageKeys.getCounter());
return cipher.doFinal(plaintext);
} catch (IllegalBlockSizeException e) {
throw new AssertionError(e);
} catch (BadPaddingException e) {
throw new AssertionError(e);
}
}
private byte[] getPlaintext(MessageKeys messageKeys, byte[] cipherText) {
try {
Cipher cipher = getCipher(Cipher.DECRYPT_MODE,
messageKeys.getCipherKey(),
messageKeys.getCounter());
return cipher.doFinal(cipherText);
} catch (IllegalBlockSizeException e) {
throw new AssertionError(e);
} catch (BadPaddingException e) {
throw new AssertionError(e);
}
}
private Cipher getCipher(int mode, SecretKeySpec key, int counter) {
try {
Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
byte[] ivBytes = new byte[16];
Conversions.intToByteArray(ivBytes, 0, counter);
IvParameterSpec iv = new IvParameterSpec(ivBytes);
cipher.init(mode, key, iv);
return cipher;
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
} catch (NoSuchPaddingException e) {
throw new AssertionError(e);
} catch (java.security.InvalidKeyException e) {
throw new AssertionError(e);
} catch (InvalidAlgorithmParameterException e) {
throw new AssertionError(e);
}
}
private SessionRecordV2 getSessionRecord() {
return new SessionRecordV2(context, masterSecret, recipient);
}
}

View File

@ -1,102 +0,0 @@
/**
* Copyright (C) 2013 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.crypto;
import android.util.Log;
import org.whispersystems.textsecure.crypto.ecc.Curve;
import org.whispersystems.textsecure.crypto.ecc.ECPublicKey;
import org.whispersystems.textsecure.crypto.kdf.DerivedSecrets;
import org.whispersystems.textsecure.crypto.kdf.HKDF;
import org.whispersystems.textsecure.crypto.kdf.KDF;
import org.whispersystems.textsecure.crypto.kdf.NKDF;
import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage;
import org.whispersystems.textsecure.util.Conversions;
import java.util.LinkedList;
import java.util.List;
public class SharedSecretCalculator {
public static DerivedSecrets calculateSharedSecret(boolean isLowEnd, KeyPair localKeyPair,
int localKeyId,
IdentityKeyPair localIdentityKeyPair,
ECPublicKey remoteKey,
int remoteKeyId,
IdentityKey remoteIdentityKey)
throws InvalidKeyException
{
Log.w("SharedSecretCalculator", "Calculating shared secret with 3DHE agreement...");
KDF kdf = new HKDF();
List<byte[]> results = new LinkedList<byte[]>();
if (isSmaller(localKeyPair.getPublicKey().getKey(), remoteKey)) {
results.add(Curve.calculateAgreement(remoteKey, localIdentityKeyPair.getPrivateKey()));
results.add(Curve.calculateAgreement(remoteIdentityKey.getPublicKey(),
localKeyPair.getPrivateKey()));
} else {
results.add(Curve.calculateAgreement(remoteIdentityKey.getPublicKey(),
localKeyPair.getPrivateKey()));
results.add(Curve.calculateAgreement(remoteKey, localIdentityKeyPair.getPrivateKey()));
}
results.add(Curve.calculateAgreement(remoteKey, localKeyPair.getPrivateKey()));
return kdf.deriveSecrets(results, isLowEnd, getInfo(localKeyId, remoteKeyId));
}
public static DerivedSecrets calculateSharedSecret(int messageVersion, boolean isLowEnd,
KeyPair localKeyPair, int localKeyId,
ECPublicKey remoteKey, int remoteKeyId)
throws InvalidKeyException
{
Log.w("SharedSecretCalculator", "Calculating shared secret with standard agreement...");
KDF kdf;
if (messageVersion >= CiphertextMessage.DHE3_INTRODUCED_VERSION) kdf = new HKDF();
else kdf = new NKDF();
Log.w("SharedSecretCalculator", "Using kdf: " + kdf);
List<byte[]> results = new LinkedList<byte[]>();
results.add(Curve.calculateAgreement(remoteKey, localKeyPair.getPrivateKey()));
return kdf.deriveSecrets(results, isLowEnd, getInfo(localKeyId, remoteKeyId));
}
private static byte[] getInfo(int localKeyId, int remoteKeyId) {
byte[] info = new byte[3 * 2];
if (localKeyId < remoteKeyId) {
Conversions.mediumToByteArray(info, 0, localKeyId);
Conversions.mediumToByteArray(info, 3, remoteKeyId);
} else {
Conversions.mediumToByteArray(info, 0, remoteKeyId);
Conversions.mediumToByteArray(info, 3, localKeyId);
}
return info;
}
private static boolean isSmaller(ECPublicKey localPublic,
ECPublicKey remotePublic)
{
return localPublic.compareTo(remotePublic) < 0;
}
}

View File

@ -36,10 +36,10 @@ public class Curve {
} }
public static ECKeyPair generateKeyPairForSession(int messageVersion) { public static ECKeyPair generateKeyPairForSession(int messageVersion) {
if (messageVersion >= CiphertextMessage.CURVE25519_INTRODUCED_VERSION) { if (messageVersion <= CiphertextMessage.LEGACY_VERSION) {
return generateKeyPairForType(DJB_TYPE);
} else {
return generateKeyPairForType(NIST_TYPE); return generateKeyPairForType(NIST_TYPE);
} else {
return generateKeyPairForType(DJB_TYPE);
} }
} }

View File

@ -27,7 +27,6 @@ public class ECKeyPair {
this.privateKey = privateKey; this.privateKey = privateKey;
} }
public ECPublicKey getPublicKey() { public ECPublicKey getPublicKey() {
return publicKey; return publicKey;
} }

View File

@ -26,50 +26,38 @@ import java.util.List;
import javax.crypto.Mac; import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec; import javax.crypto.spec.SecretKeySpec;
public class HKDF extends KDF { public class HKDF {
private static final int HASH_OUTPUT_SIZE = 32; private static final int HASH_OUTPUT_SIZE = 32;
private static final int KEY_MATERIAL_SIZE = 72; private static final int KEY_MATERIAL_SIZE = 64;
private static final int CIPHER_KEYS_OFFSET = 0; private static final int CIPHER_KEYS_OFFSET = 0;
private static final int MAC_KEYS_OFFSET = 32; private static final int MAC_KEYS_OFFSET = 32;
@Override public DerivedSecrets deriveSecrets(byte[] inputKeyMaterial, byte[] info) {
public DerivedSecrets deriveSecrets(List<byte[]> sharedSecret, byte[] salt = new byte[HASH_OUTPUT_SIZE];
boolean isLowEnd, byte[] info) return deriveSecrets(inputKeyMaterial, salt, info);
{ }
byte[] inputKeyMaterial = concatenateSharedSecrets(sharedSecret);
byte[] salt = new byte[HASH_OUTPUT_SIZE]; public DerivedSecrets deriveSecrets(byte[] inputKeyMaterial, byte[] salt, byte[] info) {
byte[] prk = extract(salt, inputKeyMaterial); byte[] prk = extract(salt, inputKeyMaterial);
byte[] okm = expand(prk, info, KEY_MATERIAL_SIZE); byte[] okm = expand(prk, info, KEY_MATERIAL_SIZE);
SecretKeySpec cipherKey = deriveCipherKey(okm, isLowEnd); SecretKeySpec cipherKey = deriveCipherKey(okm);
SecretKeySpec macKey = deriveMacKey(okm, isLowEnd); SecretKeySpec macKey = deriveMacKey(okm);
return new DerivedSecrets(cipherKey, macKey); return new DerivedSecrets(cipherKey, macKey);
} }
private SecretKeySpec deriveCipherKey(byte[] okm, boolean isLowEnd) { private SecretKeySpec deriveCipherKey(byte[] okm) {
byte[] cipherKey = new byte[16]; byte[] cipherKey = new byte[32];
System.arraycopy(okm, CIPHER_KEYS_OFFSET, cipherKey, 0, cipherKey.length);
if (isLowEnd) {
System.arraycopy(okm, CIPHER_KEYS_OFFSET + 0, cipherKey, 0, cipherKey.length);
} else {
System.arraycopy(okm, CIPHER_KEYS_OFFSET + 16, cipherKey, 0, cipherKey.length);
}
return new SecretKeySpec(cipherKey, "AES"); return new SecretKeySpec(cipherKey, "AES");
} }
private SecretKeySpec deriveMacKey(byte[] okm, boolean isLowEnd) { private SecretKeySpec deriveMacKey(byte[] okm) {
byte[] macKey = new byte[20]; byte[] macKey = new byte[32];
System.arraycopy(okm, MAC_KEYS_OFFSET, macKey, 0, macKey.length);
if (isLowEnd) {
System.arraycopy(okm, MAC_KEYS_OFFSET + 0, macKey, 0, macKey.length);
} else {
System.arraycopy(okm, MAC_KEYS_OFFSET + 20, macKey, 0, macKey.length);
}
return new SecretKeySpec(macKey, "HmacSHA1"); return new SecretKeySpec(macKey, "HmacSHA1");
} }
@ -96,7 +84,9 @@ public class HKDF extends KDF {
mac.init(new SecretKeySpec(prk, "HmacSHA256")); mac.init(new SecretKeySpec(prk, "HmacSHA256"));
mac.update(mixin); mac.update(mixin);
mac.update(info); if (info != null) {
mac.update(info);
}
mac.update((byte)i); mac.update((byte)i);
byte[] stepResult = mac.doFinal(); byte[] stepResult = mac.doFinal();

View File

@ -1,45 +0,0 @@
/**
* Copyright (C) 2013 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.crypto.kdf;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.util.LinkedList;
import java.util.List;
public abstract class KDF {
public abstract DerivedSecrets deriveSecrets(List<byte[]> sharedSecret,
boolean isLowEnd, byte[] info);
protected byte[] concatenateSharedSecrets(List<byte[]> sharedSecrets) {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
for (byte[] sharedSecret : sharedSecrets) {
baos.write(sharedSecret);
}
return baos.toByteArray();
} catch (IOException e) {
throw new AssertionError(e);
}
}
}

View File

@ -23,15 +23,15 @@ import org.whispersystems.textsecure.util.Conversions;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.util.List;
import javax.crypto.spec.SecretKeySpec; import javax.crypto.spec.SecretKeySpec;
public class NKDF extends KDF { public class NKDF {
@Override public static final int LEGACY_CIPHER_KEY_LENGTH = 16;
public DerivedSecrets deriveSecrets(List<byte[]> sharedSecret, public static final int LEGACY_MAC_KEY_LENGTH = 20;
boolean isLowEnd, byte[] info)
public DerivedSecrets deriveSecrets(byte[] sharedSecret, boolean isLowEnd)
{ {
SecretKeySpec cipherKey = deriveCipherSecret(isLowEnd, sharedSecret); SecretKeySpec cipherKey = deriveCipherSecret(isLowEnd, sharedSecret);
SecretKeySpec macKey = deriveMacSecret(cipherKey); SecretKeySpec macKey = deriveMacSecret(cipherKey);
@ -39,15 +39,14 @@ public class NKDF extends KDF {
return new DerivedSecrets(cipherKey, macKey); return new DerivedSecrets(cipherKey, macKey);
} }
private SecretKeySpec deriveCipherSecret(boolean isLowEnd, List<byte[]> sharedSecret) { private SecretKeySpec deriveCipherSecret(boolean isLowEnd, byte[] sharedSecret) {
byte[] sharedSecretBytes = concatenateSharedSecrets(sharedSecret); byte[] derivedBytes = deriveBytes(sharedSecret, LEGACY_CIPHER_KEY_LENGTH * 2);
byte[] derivedBytes = deriveBytes(sharedSecretBytes, 16 * 2); byte[] cipherSecret = new byte[LEGACY_CIPHER_KEY_LENGTH];
byte[] cipherSecret = new byte[16];
if (isLowEnd) { if (isLowEnd) {
System.arraycopy(derivedBytes, 16, cipherSecret, 0, 16); System.arraycopy(derivedBytes, LEGACY_CIPHER_KEY_LENGTH, cipherSecret, 0, LEGACY_CIPHER_KEY_LENGTH);
} else { } else {
System.arraycopy(derivedBytes, 0, cipherSecret, 0, 16); System.arraycopy(derivedBytes, 0, cipherSecret, 0, LEGACY_CIPHER_KEY_LENGTH);
} }
return new SecretKeySpec(cipherSecret, "AES"); return new SecretKeySpec(cipherSecret, "AES");
@ -84,6 +83,4 @@ public class NKDF extends KDF {
return md.digest(); return md.digest();
} }
} }

View File

@ -1,145 +1,18 @@
package org.whispersystems.textsecure.crypto.protocol; package org.whispersystems.textsecure.crypto.protocol;
import org.whispersystems.textsecure.crypto.InvalidMacException; public interface CiphertextMessage {
import org.whispersystems.textsecure.crypto.InvalidMessageException;
import org.whispersystems.textsecure.crypto.MessageMac;
import org.whispersystems.textsecure.crypto.PublicKey;
import org.whispersystems.textsecure.crypto.SessionCipher;
import org.whispersystems.textsecure.util.Conversions;
public class CiphertextMessage { public static final int LEGACY_VERSION = 1;
public static final int CURRENT_VERSION = 2;
public static final int SUPPORTED_VERSION = 2; public static final int LEGACY_WHISPER_TYPE = 1;
public static final int DHE3_INTRODUCED_VERSION = 2; public static final int CURRENT_WHISPER_TYPE = 2;
public static final int CURVE25519_INTRODUCED_VERSION = 2; public static final int PREKEY_WHISPER_TYPE = 3;
static final int VERSION_LENGTH = 1; // This should be the worst case (worse than V2). So not always accurate, but good enough for padding.
private static final int SENDER_KEY_ID_LENGTH = 3; public static final int ENCRYPTED_MESSAGE_OVERHEAD = WhisperMessageV1.ENCRYPTED_MESSAGE_OVERHEAD;
private static final int RECEIVER_KEY_ID_LENGTH = 3;
private static final int NEXT_KEY_LENGTH = PublicKey.KEY_SIZE;
private static final int COUNTER_LENGTH = 3;
private static final int HEADER_LENGTH = VERSION_LENGTH + SENDER_KEY_ID_LENGTH +
RECEIVER_KEY_ID_LENGTH + COUNTER_LENGTH +
NEXT_KEY_LENGTH;
static final int VERSION_OFFSET = 0; public byte[] serialize();
private static final int SENDER_KEY_ID_OFFSET = VERSION_OFFSET + VERSION_LENGTH; public int getType();
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;
private static final int COUNTER_OFFSET = NEXT_KEY_OFFSET + NEXT_KEY_LENGTH;
private static final int BODY_OFFSET = COUNTER_OFFSET + COUNTER_LENGTH;
public static final int ENCRYPTED_MESSAGE_OVERHEAD = HEADER_LENGTH + MessageMac.MAC_LENGTH; }
private final byte[] ciphertext;
public CiphertextMessage(SessionCipher.SessionCipherContext sessionContext, byte[] ciphertextBody) {
this.ciphertext = new byte[HEADER_LENGTH + ciphertextBody.length + MessageMac.MAC_LENGTH];
setVersion(sessionContext.getMessageVersion(), SUPPORTED_VERSION);
setSenderKeyId(sessionContext.getSenderKeyId());
setReceiverKeyId(sessionContext.getRecipientKeyId());
setNextKeyBytes(sessionContext.getNextKey().serialize());
setCounter(sessionContext.getCounter());
setBody(ciphertextBody);
setMac(MessageMac.calculateMac(ciphertext, 0, ciphertext.length - MessageMac.MAC_LENGTH,
sessionContext.getSessionKey().getMacKey()));
}
public CiphertextMessage(byte[] ciphertext) throws InvalidMessageException {
this.ciphertext = ciphertext;
if (ciphertext.length < HEADER_LENGTH) {
throw new InvalidMessageException("Not long enough for ciphertext header!");
}
if (getCurrentVersion() > SUPPORTED_VERSION) {
throw new InvalidMessageException("Unspported version: " + getCurrentVersion());
}
}
public void setVersion(int current, int supported) {
ciphertext[VERSION_OFFSET] = Conversions.intsToByteHighAndLow(current, supported);
}
public int getCurrentVersion() {
return Conversions.highBitsToInt(ciphertext[VERSION_OFFSET]);
}
public int getSupportedVersion() {
return Conversions.lowBitsToInt(ciphertext[VERSION_OFFSET]);
}
public void setSenderKeyId(int senderKeyId) {
Conversions.mediumToByteArray(ciphertext, SENDER_KEY_ID_OFFSET, senderKeyId);
}
public int getSenderKeyId() {
return Conversions.byteArrayToMedium(ciphertext, SENDER_KEY_ID_OFFSET);
}
public void setReceiverKeyId(int receiverKeyId) {
Conversions.mediumToByteArray(ciphertext, RECEIVER_KEY_ID_OFFSET, receiverKeyId);
}
public int getReceiverKeyId() {
return Conversions.byteArrayToMedium(ciphertext, RECEIVER_KEY_ID_OFFSET);
}
public void setNextKeyBytes(byte[] nextKey) {
assert(nextKey.length == NEXT_KEY_LENGTH);
System.arraycopy(nextKey, 0, ciphertext, NEXT_KEY_OFFSET, nextKey.length);
}
public byte[] getNextKeyBytes() {
byte[] nextKeyBytes = new byte[NEXT_KEY_LENGTH];
System.arraycopy(ciphertext, NEXT_KEY_OFFSET, nextKeyBytes, 0, nextKeyBytes.length);
return nextKeyBytes;
}
public void setCounter(int counter) {
Conversions.mediumToByteArray(ciphertext, COUNTER_OFFSET, counter);
}
public int getCounter() {
return Conversions.byteArrayToMedium(ciphertext, COUNTER_OFFSET);
}
public void setBody(byte[] body) {
System.arraycopy(body, 0, ciphertext, BODY_OFFSET, body.length);
}
public byte[] getBody() {
byte[] body = new byte[ciphertext.length - HEADER_LENGTH - MessageMac.MAC_LENGTH];
System.arraycopy(ciphertext, BODY_OFFSET, body, 0, body.length);
return body;
}
public void setMac(byte[] mac) {
System.arraycopy(mac, 0, ciphertext, ciphertext.length-mac.length, mac.length);
}
public byte[] getMac() {
byte[] mac = new byte[MessageMac.MAC_LENGTH];
System.arraycopy(ciphertext, ciphertext.length-mac.length, mac, 0, mac.length);
return mac;
}
public byte[] serialize() {
return ciphertext;
}
public void verifyMac(SessionCipher.SessionCipherContext sessionContext)
throws InvalidMessageException
{
try {
MessageMac.verifyMac(this.ciphertext, 0, this.ciphertext.length - MessageMac.MAC_LENGTH,
getMac(), sessionContext.getSessionKey().getMacKey());
} catch (InvalidMacException e) {
throw new InvalidMessageException(e);
}
}
}

View File

@ -1,108 +0,0 @@
/**
* Copyright (C) 2013 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.crypto.protocol;
import org.whispersystems.textsecure.crypto.IdentityKey;
import org.whispersystems.textsecure.crypto.InvalidKeyException;
import org.whispersystems.textsecure.crypto.InvalidMessageException;
import org.whispersystems.textsecure.crypto.InvalidVersionException;
import org.whispersystems.textsecure.crypto.PublicKey;
import org.whispersystems.textsecure.util.Conversions;
/**
* 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 {
public static final int SUPPORTED_VERSION = CiphertextMessage.SUPPORTED_VERSION;
private static final int VERSION_LENGTH = CiphertextMessage.VERSION_LENGTH;
private static final int IDENTITY_KEY_LENGTH = IdentityKey.SIZE;
private static final int VERSION_OFFSET = CiphertextMessage.VERSION_OFFSET;
private static final int IDENTITY_KEY_OFFSET = VERSION_OFFSET + VERSION_LENGTH;
private final byte[] messageBytes;
private final CiphertextMessage bundledMessage;
private final IdentityKey identityKey;
public PreKeyBundleMessage(byte[] messageBytes)
throws InvalidVersionException, InvalidKeyException
{
try {
this.messageBytes = messageBytes;
int messageVersion = Conversions.highBitsToInt(this.messageBytes[VERSION_OFFSET]);
if (messageVersion > CiphertextMessage.SUPPORTED_VERSION)
throw new InvalidVersionException("Key exchange with version: " + messageVersion);
this.identityKey = new IdentityKey(messageBytes, IDENTITY_KEY_OFFSET);
byte[] bundledMessageBytes = new byte[messageBytes.length - IDENTITY_KEY_LENGTH];
bundledMessageBytes[VERSION_OFFSET] = this.messageBytes[VERSION_OFFSET];
System.arraycopy(messageBytes, IDENTITY_KEY_OFFSET+IDENTITY_KEY_LENGTH, bundledMessageBytes,
VERSION_OFFSET+VERSION_LENGTH, bundledMessageBytes.length-VERSION_LENGTH);
this.bundledMessage = new CiphertextMessage(bundledMessageBytes);
} catch (InvalidMessageException e) {
throw new InvalidKeyException(e);
}
}
public PreKeyBundleMessage(CiphertextMessage bundledMessage, IdentityKey identityKey) {
this.bundledMessage = bundledMessage;
this.identityKey = identityKey;
this.messageBytes = new byte[IDENTITY_KEY_LENGTH + bundledMessage.serialize().length];
byte[] bundledMessageBytes = bundledMessage.serialize();
byte[] identityKeyBytes = identityKey.serialize();
messageBytes[VERSION_OFFSET] = bundledMessageBytes[VERSION_OFFSET];
System.arraycopy(identityKeyBytes, 0, messageBytes, IDENTITY_KEY_OFFSET, identityKeyBytes.length);
System.arraycopy(bundledMessageBytes, VERSION_OFFSET+VERSION_LENGTH,
messageBytes, IDENTITY_KEY_OFFSET+IDENTITY_KEY_LENGTH,
bundledMessageBytes.length-VERSION_LENGTH);
}
public byte[] serialize() {
return this.messageBytes;
}
public int getSupportedVersion() {
return bundledMessage.getSupportedVersion();
}
public IdentityKey getIdentityKey() {
return identityKey;
}
public PublicKey getPublicKey() throws InvalidKeyException {
return new PublicKey(bundledMessage.getNextKeyBytes());
}
public CiphertextMessage getBundledMessage() {
return bundledMessage;
}
public int getPreKeyId() {
return bundledMessage.getReceiverKeyId();
}
}

View File

@ -0,0 +1,104 @@
package org.whispersystems.textsecure.crypto.protocol;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import org.whispersystems.textsecure.crypto.IdentityKey;
import org.whispersystems.textsecure.crypto.InvalidKeyException;
import org.whispersystems.textsecure.crypto.InvalidMessageException;
import org.whispersystems.textsecure.crypto.InvalidVersionException;
import org.whispersystems.textsecure.crypto.ecc.Curve;
import org.whispersystems.textsecure.crypto.ecc.ECPublicKey;
import org.whispersystems.textsecure.util.Conversions;
import org.whispersystems.textsecure.util.Util;
public class PreKeyWhisperMessage implements CiphertextMessage {
private final int version;
private final int preKeyId;
private final ECPublicKey baseKey;
private final IdentityKey identityKey;
private final WhisperMessageV2 message;
private final byte[] serialized;
public PreKeyWhisperMessage(byte[] serialized)
throws InvalidMessageException, InvalidVersionException
{
try {
this.version = Conversions.lowBitsToInt(serialized[0]);
if (this.version > CiphertextMessage.CURRENT_VERSION) {
throw new InvalidVersionException("Unknown version: " + this.version);
}
WhisperProtos.PreKeyWhisperMessage preKeyWhisperMessage
= WhisperProtos.PreKeyWhisperMessage.parseFrom(ByteString.copyFrom(serialized, 1,
serialized.length-1));
if (!preKeyWhisperMessage.hasPreKeyId() ||
!preKeyWhisperMessage.hasBaseKey() ||
!preKeyWhisperMessage.hasIdentityKey() ||
!preKeyWhisperMessage.hasMessage())
{
throw new InvalidMessageException("Incomplete message.");
}
this.serialized = serialized;
this.preKeyId = preKeyWhisperMessage.getPreKeyId();
this.baseKey = Curve.decodePoint(preKeyWhisperMessage.getBaseKey().toByteArray(), 0);
this.identityKey = new IdentityKey(Curve.decodePoint(preKeyWhisperMessage.getIdentityKey().toByteArray(), 0));
this.message = new WhisperMessageV2(preKeyWhisperMessage.getMessage().toByteArray());
} catch (InvalidProtocolBufferException e) {
throw new InvalidMessageException(e);
} catch (InvalidKeyException e) {
throw new InvalidMessageException(e);
}
}
public PreKeyWhisperMessage(int preKeyId, ECPublicKey baseKey, IdentityKey identityKey,
WhisperMessageV2 message)
{
this.version = CiphertextMessage.CURRENT_VERSION;
this.preKeyId = preKeyId;
this.baseKey = baseKey;
this.identityKey = identityKey;
this.message = message;
byte[] versionBytes = {Conversions.intsToByteHighAndLow(CURRENT_VERSION, this.version)};
byte[] messageBytes = WhisperProtos.PreKeyWhisperMessage.newBuilder()
.setPreKeyId(preKeyId)
.setBaseKey(ByteString.copyFrom(baseKey.serialize()))
.setIdentityKey(ByteString.copyFrom(identityKey.serialize()))
.setMessage(ByteString.copyFrom(message.serialize()))
.build().toByteArray();
this.serialized = Util.combine(versionBytes, messageBytes);
}
public IdentityKey getIdentityKey() {
return identityKey;
}
public int getPreKeyId() {
return preKeyId;
}
public ECPublicKey getBaseKey() {
return baseKey;
}
public WhisperMessageV2 getWhisperMessage() {
return message;
}
@Override
public byte[] serialize() {
return serialized;
}
@Override
public int getType() {
return CiphertextMessage.PREKEY_WHISPER_TYPE;
}
}

View File

@ -0,0 +1,187 @@
package org.whispersystems.textsecure.crypto.protocol;
import android.util.Log;
import org.whispersystems.textsecure.crypto.InvalidMessageException;
import org.whispersystems.textsecure.crypto.PublicKey;
import org.whispersystems.textsecure.crypto.SessionCipherV1;
import org.whispersystems.textsecure.util.Conversions;
import org.whispersystems.textsecure.util.Hex;
import org.whispersystems.textsecure.util.Util;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public class WhisperMessageV1 implements CiphertextMessage{
private 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;
private static final int COUNTER_LENGTH = 3;
private static final int HEADER_LENGTH = VERSION_LENGTH + SENDER_KEY_ID_LENGTH +
RECEIVER_KEY_ID_LENGTH + COUNTER_LENGTH +
NEXT_KEY_LENGTH;
private static final int MAC_LENGTH = 10;
private 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;
private static final int COUNTER_OFFSET = NEXT_KEY_OFFSET + NEXT_KEY_LENGTH;
private static final int BODY_OFFSET = COUNTER_OFFSET + COUNTER_LENGTH;
static final int ENCRYPTED_MESSAGE_OVERHEAD = HEADER_LENGTH + MAC_LENGTH;
private final byte[] ciphertext;
public WhisperMessageV1(SessionCipherV1.SessionCipherContext sessionContext,
byte[] ciphertextBody)
{
this.ciphertext = new byte[HEADER_LENGTH + ciphertextBody.length + MAC_LENGTH];
setVersion(sessionContext.getMessageVersion(), CURRENT_VERSION);
setSenderKeyId(sessionContext.getSenderKeyId());
setReceiverKeyId(sessionContext.getRecipientKeyId());
setNextKeyBytes(sessionContext.getNextKey().serialize());
setCounter(sessionContext.getCounter());
setBody(ciphertextBody);
setMac(calculateMac(sessionContext.getSessionKey().getMacKey(),
ciphertext, 0, ciphertext.length - MAC_LENGTH));
}
public WhisperMessageV1(byte[] ciphertext) throws InvalidMessageException {
this.ciphertext = ciphertext;
if (ciphertext.length < HEADER_LENGTH) {
throw new InvalidMessageException("Not long enough for ciphertext header!");
}
if (getCurrentVersion() > LEGACY_VERSION) {
throw new InvalidMessageException("Received non-legacy version: " + getCurrentVersion());
}
}
public void setVersion(int current, int supported) {
ciphertext[VERSION_OFFSET] = Conversions.intsToByteHighAndLow(current, supported);
}
public int getCurrentVersion() {
return Conversions.highBitsToInt(ciphertext[VERSION_OFFSET]);
}
public int getSupportedVersion() {
return Conversions.lowBitsToInt(ciphertext[VERSION_OFFSET]);
}
public void setSenderKeyId(int senderKeyId) {
Conversions.mediumToByteArray(ciphertext, SENDER_KEY_ID_OFFSET, senderKeyId);
}
public int getSenderKeyId() {
return Conversions.byteArrayToMedium(ciphertext, SENDER_KEY_ID_OFFSET);
}
public void setReceiverKeyId(int receiverKeyId) {
Conversions.mediumToByteArray(ciphertext, RECEIVER_KEY_ID_OFFSET, receiverKeyId);
}
public int getReceiverKeyId() {
return Conversions.byteArrayToMedium(ciphertext, RECEIVER_KEY_ID_OFFSET);
}
public void setNextKeyBytes(byte[] nextKey) {
assert(nextKey.length == NEXT_KEY_LENGTH);
System.arraycopy(nextKey, 0, ciphertext, NEXT_KEY_OFFSET, nextKey.length);
}
public byte[] getNextKeyBytes() {
byte[] nextKeyBytes = new byte[NEXT_KEY_LENGTH];
System.arraycopy(ciphertext, NEXT_KEY_OFFSET, nextKeyBytes, 0, nextKeyBytes.length);
return nextKeyBytes;
}
public void setCounter(int counter) {
Conversions.mediumToByteArray(ciphertext, COUNTER_OFFSET, counter);
}
public int getCounter() {
return Conversions.byteArrayToMedium(ciphertext, COUNTER_OFFSET);
}
public void setBody(byte[] body) {
System.arraycopy(body, 0, ciphertext, BODY_OFFSET, body.length);
}
public byte[] getBody() {
byte[] body = new byte[ciphertext.length - HEADER_LENGTH - MAC_LENGTH];
System.arraycopy(ciphertext, BODY_OFFSET, body, 0, body.length);
return body;
}
public void setMac(byte[] mac) {
System.arraycopy(mac, 0, ciphertext, ciphertext.length-mac.length, mac.length);
}
public byte[] getMac() {
byte[] mac = new byte[MAC_LENGTH];
System.arraycopy(ciphertext, ciphertext.length-mac.length, mac, 0, mac.length);
return mac;
}
@Override
public byte[] serialize() {
return ciphertext;
}
@Override
public int getType() {
return CiphertextMessage.LEGACY_WHISPER_TYPE;
}
public void verifyMac(SessionCipherV1.SessionCipherContext sessionContext)
throws InvalidMessageException
{
verifyMac(sessionContext.getSessionKey().getMacKey(),
this.ciphertext, 0, this.ciphertext.length - MAC_LENGTH, getMac());
}
private byte[] calculateMac(SecretKeySpec macKey, byte[] message, int offset, int length) {
try {
Mac mac = Mac.getInstance("HmacSHA1");
mac.init(macKey);
mac.update(message, offset, length);
byte[] macBytes = mac.doFinal();
return Util.trim(macBytes, MAC_LENGTH);
} catch (NoSuchAlgorithmException e) {
throw new IllegalArgumentException(e);
} catch (InvalidKeyException e) {
throw new IllegalArgumentException(e);
}
}
private void verifyMac(SecretKeySpec macKey, byte[] message, int offset, int length,
byte[] receivedMac)
throws InvalidMessageException
{
byte[] localMac = calculateMac(macKey, message, offset, length);
Log.w("WhisperMessageV1", "Local Mac: " + Hex.toString(localMac));
Log.w("WhisperMessageV1", "Remot Mac: " + Hex.toString(receivedMac));
if (!Arrays.equals(localMac, receivedMac)) {
throw new InvalidMessageException("MAC on message does not match calculated MAC.");
}
}
}

View File

@ -0,0 +1,132 @@
package org.whispersystems.textsecure.crypto.protocol;
import android.util.Log;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import org.whispersystems.textsecure.crypto.InvalidKeyException;
import org.whispersystems.textsecure.crypto.InvalidMessageException;
import org.whispersystems.textsecure.crypto.ecc.Curve;
import org.whispersystems.textsecure.crypto.ecc.ECPublicKey;
import org.whispersystems.textsecure.crypto.protocol.WhisperProtos.WhisperMessage;
import org.whispersystems.textsecure.util.Conversions;
import org.whispersystems.textsecure.util.Hex;
import org.whispersystems.textsecure.util.Util;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public class WhisperMessageV2 implements CiphertextMessage {
private static final int MAC_LENGTH = 8;
private final ECPublicKey senderEphemeral;
private final int counter;
private final int previousCounter;
private final byte[] ciphertext;
private final byte[] serialized;
public WhisperMessageV2(byte[] serialized) throws InvalidMessageException {
try {
byte[][] messageParts = Util.split(serialized, 1, serialized.length - 1 - MAC_LENGTH, MAC_LENGTH);
byte version = messageParts[0][0];
byte[] message = messageParts[1];
byte[] mac = messageParts[2];
if (Conversions.highBitsToInt(version) != CURRENT_VERSION) {
throw new InvalidMessageException("Unknown version: " + Conversions.lowBitsToInt(version));
}
WhisperMessage whisperMessage = WhisperMessage.parseFrom(message);
if (!whisperMessage.hasCiphertext() ||
!whisperMessage.hasCounter() ||
!whisperMessage.hasEphemeralKey())
{
throw new InvalidMessageException("Incomplete message.");
}
this.serialized = serialized;
this.senderEphemeral = Curve.decodePoint(whisperMessage.getEphemeralKey().toByteArray(), 0);
this.counter = whisperMessage.getCounter();
this.previousCounter = whisperMessage.getPreviousCounter();
this.ciphertext = whisperMessage.getCiphertext().toByteArray();
} catch (InvalidProtocolBufferException e) {
throw new InvalidMessageException(e);
} catch (InvalidKeyException e) {
throw new InvalidMessageException(e);
}
}
public WhisperMessageV2(SecretKeySpec macKey, ECPublicKey senderEphemeral,
int counter, int previousCounter, byte[] ciphertext)
{
byte[] version = {Conversions.intsToByteHighAndLow(CURRENT_VERSION, CURRENT_VERSION)};
byte[] message = WhisperMessage.newBuilder()
.setEphemeralKey(ByteString.copyFrom(senderEphemeral.serialize()))
.setCounter(counter)
.setPreviousCounter(previousCounter)
.setCiphertext(ByteString.copyFrom(ciphertext))
.build().toByteArray();
byte[] mac = getMac(macKey, Util.combine(version, message));
this.serialized = Util.combine(version, message, mac);
this.senderEphemeral = senderEphemeral;
this.counter = counter;
this.previousCounter = previousCounter;
this.ciphertext = ciphertext;
}
public ECPublicKey getSenderEphemeral() {
return senderEphemeral;
}
public int getCounter() {
return counter;
}
public byte[] getBody() {
return ciphertext;
}
public void verifyMac(SecretKeySpec macKey)
throws InvalidMessageException
{
byte[][] parts = Util.split(serialized, serialized.length - MAC_LENGTH, MAC_LENGTH);
byte[] ourMac = getMac(macKey, parts[0]);
byte[] theirMac = parts[1];
if (!Arrays.equals(ourMac, theirMac)) {
throw new InvalidMessageException("Bad Mac!");
}
}
private byte[] getMac(SecretKeySpec macKey, byte[] serialized) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(macKey);
byte[] fullMac = mac.doFinal(serialized);
return Util.trim(fullMac, MAC_LENGTH);
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
} catch (java.security.InvalidKeyException e) {
throw new AssertionError(e);
}
}
@Override
public byte[] serialize() {
return serialized;
}
@Override
public int getType() {
return CiphertextMessage.CURRENT_WHISPER_TYPE;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,58 @@
package org.whispersystems.textsecure.crypto.ratchet;
import org.whispersystems.textsecure.crypto.kdf.DerivedSecrets;
import org.whispersystems.textsecure.crypto.kdf.HKDF;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public class ChainKey {
private static final byte[] MESSAGE_KEY_SEED = {0x01};
private static final byte[] CHAIN_KEY_SEED = {0x02};
private final byte[] key;
private final int index;
public ChainKey(byte[] key, int index) {
this.key = key;
this.index = index;
}
public byte[] getKey() {
return key;
}
public int getIndex() {
return index;
}
public ChainKey getNextChainKey() {
byte[] nextKey = getBaseMaterial(CHAIN_KEY_SEED);
return new ChainKey(nextKey, index + 1);
}
public MessageKeys getMessageKeys() {
HKDF kdf = new HKDF();
byte[] inputKeyMaterial = getBaseMaterial(MESSAGE_KEY_SEED);
DerivedSecrets keyMaterial = kdf.deriveSecrets(inputKeyMaterial, null);
return new MessageKeys(keyMaterial.getCipherKey(), keyMaterial.getMacKey(), index);
}
private byte[] getBaseMaterial(byte[] seed) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(key, "HmacSHA256"));
return mac.doFinal(seed);
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
} catch (InvalidKeyException e) {
throw new AssertionError(e);
}
}
}

View File

@ -0,0 +1,28 @@
package org.whispersystems.textsecure.crypto.ratchet;
import javax.crypto.spec.SecretKeySpec;
public class MessageKeys {
private final SecretKeySpec cipherKey;
private final SecretKeySpec macKey;
private final int counter;
public MessageKeys(SecretKeySpec cipherKey, SecretKeySpec macKey, int counter) {
this.cipherKey = cipherKey;
this.macKey = macKey;
this.counter = counter;
}
public SecretKeySpec getCipherKey() {
return cipherKey;
}
public SecretKeySpec getMacKey() {
return macKey;
}
public int getCounter() {
return counter;
}
}

View File

@ -0,0 +1,120 @@
package org.whispersystems.textsecure.crypto.ratchet;
import android.util.Pair;
import org.whispersystems.textsecure.crypto.IdentityKey;
import org.whispersystems.textsecure.crypto.IdentityKeyPair;
import org.whispersystems.textsecure.crypto.InvalidKeyException;
import org.whispersystems.textsecure.crypto.ecc.Curve;
import org.whispersystems.textsecure.crypto.ecc.ECKeyPair;
import org.whispersystems.textsecure.crypto.ecc.ECPublicKey;
import org.whispersystems.textsecure.crypto.kdf.DerivedSecrets;
import org.whispersystems.textsecure.crypto.kdf.HKDF;
import org.whispersystems.textsecure.storage.SessionRecordV2;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
public class RatchetingSession {
public static void initializeSession(SessionRecordV2 sessionRecord,
ECKeyPair ourBaseKey,
ECPublicKey theirBaseKey,
ECKeyPair ourEphemeralKey,
ECPublicKey theirEphemeralKey,
IdentityKeyPair ourIdentityKey,
IdentityKey theirIdentityKey)
throws InvalidKeyException
{
if (isAlice(ourBaseKey.getPublicKey(), theirBaseKey, ourEphemeralKey.getPublicKey(), theirEphemeralKey)) {
initializeSessionAsAlice(sessionRecord, ourBaseKey, theirBaseKey, theirEphemeralKey,
ourIdentityKey, theirIdentityKey);
} else {
initializeSessionAsBob(sessionRecord, ourBaseKey, theirBaseKey,
ourEphemeralKey, ourIdentityKey, theirIdentityKey);
}
}
private static void initializeSessionAsAlice(SessionRecordV2 sessionRecord,
ECKeyPair ourBaseKey, ECPublicKey theirBaseKey,
ECPublicKey theirEphemeralKey,
IdentityKeyPair ourIdentityKey,
IdentityKey theirIdentityKey)
throws InvalidKeyException
{
sessionRecord.setRemoteIdentityKey(theirIdentityKey);
sessionRecord.setLocalIdentityKey(ourIdentityKey.getPublicKey());
ECKeyPair sendingKey = Curve.generateKeyPairForType(ourIdentityKey.getPublicKey().getPublicKey().getType());
Pair<RootKey, ChainKey> receivingChain = calculate3DHE(ourBaseKey, theirBaseKey, ourIdentityKey, theirIdentityKey);
Pair<RootKey, ChainKey> sendingChain = receivingChain.first.createChain(theirEphemeralKey, sendingKey);
sessionRecord.addReceiverChain(theirEphemeralKey, receivingChain.second);
sessionRecord.setSenderChain(sendingKey, sendingChain.second);
sessionRecord.setRootKey(sendingChain.first);
}
private static void initializeSessionAsBob(SessionRecordV2 sessionRecord,
ECKeyPair ourBaseKey, ECPublicKey theirBaseKey,
ECKeyPair ourEphemeralKey,
IdentityKeyPair ourIdentityKey,
IdentityKey theirIdentityKey)
throws InvalidKeyException
{
sessionRecord.setRemoteIdentityKey(theirIdentityKey);
sessionRecord.setLocalIdentityKey(ourIdentityKey.getPublicKey());
Pair<RootKey, ChainKey> sendingChain = calculate3DHE(ourBaseKey, theirBaseKey,
ourIdentityKey, theirIdentityKey);
sessionRecord.setSenderChain(ourEphemeralKey, sendingChain.second);
sessionRecord.setRootKey(sendingChain.first);
}
public static Pair<RootKey, ChainKey> calculate3DHE(ECKeyPair ourEphemeral, ECPublicKey theirEphemeral,
IdentityKeyPair ourIdentity, IdentityKey theirIdentity)
throws InvalidKeyException
{
try {
ByteArrayOutputStream secrets = new ByteArrayOutputStream();
if (isLowEnd(ourEphemeral.getPublicKey(), theirEphemeral)) {
secrets.write(Curve.calculateAgreement(theirEphemeral, ourIdentity.getPrivateKey()));
secrets.write(Curve.calculateAgreement(theirIdentity.getPublicKey(), ourEphemeral.getPrivateKey()));
} else {
secrets.write(Curve.calculateAgreement(theirIdentity.getPublicKey(), ourEphemeral.getPrivateKey()));
secrets.write(Curve.calculateAgreement(theirEphemeral, ourIdentity.getPrivateKey()));
}
secrets.write(Curve.calculateAgreement(theirEphemeral, ourEphemeral.getPrivateKey()));
DerivedSecrets derivedSecrets = new HKDF().deriveSecrets(secrets.toByteArray(),
"WhisperText".getBytes());
return new Pair<RootKey, ChainKey>(new RootKey(derivedSecrets.getCipherKey().getEncoded()),
new ChainKey(derivedSecrets.getMacKey().getEncoded(), 0));
} catch (IOException e) {
throw new AssertionError(e);
}
}
private static boolean isAlice(ECPublicKey ourBaseKey, ECPublicKey theirBaseKey,
ECPublicKey ourEphemeralKey, ECPublicKey theirEphemeralKey)
{
if (ourEphemeralKey.equals(ourBaseKey)) {
return false;
}
if (theirEphemeralKey.equals(theirBaseKey)) {
return true;
}
return isLowEnd(ourBaseKey, theirBaseKey);
}
private static boolean isLowEnd(ECPublicKey ourKey, ECPublicKey theirKey) {
return ourKey.compareTo(theirKey) < 0;
}
}

View File

@ -0,0 +1,38 @@
package org.whispersystems.textsecure.crypto.ratchet;
import android.util.Pair;
import org.whispersystems.textsecure.crypto.InvalidKeyException;
import org.whispersystems.textsecure.crypto.ecc.Curve;
import org.whispersystems.textsecure.crypto.ecc.ECKeyPair;
import org.whispersystems.textsecure.crypto.ecc.ECPublicKey;
import org.whispersystems.textsecure.crypto.kdf.DerivedSecrets;
import org.whispersystems.textsecure.crypto.kdf.HKDF;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
public class RootKey {
private final byte[] key;
public RootKey(byte[] key) {
this.key = key;
}
public byte[] getKeyBytes() {
return key;
}
public Pair<RootKey, ChainKey> createChain(ECPublicKey theirEphemeral, ECKeyPair ourEphemeral)
throws InvalidKeyException
{
HKDF kdf = new HKDF();
byte[] sharedSecret = Curve.calculateAgreement(theirEphemeral, ourEphemeral.getPrivateKey());
DerivedSecrets keys = kdf.deriveSecrets(sharedSecret, key, "WhisperRatchet".getBytes());
RootKey newRootKey = new RootKey(keys.getMacKey().getEncoded());
ChainKey newChainKey = new ChainKey(keys.getCipherKey().getEncoded(), 0);
return new Pair<RootKey, ChainKey>(newRootKey, newChainKey);
}
}

View File

@ -30,8 +30,9 @@ import java.nio.channels.FileChannel;
public abstract class Record { public abstract class Record {
protected static final String SESSIONS_DIRECTORY = "sessions"; protected static final String SESSIONS_DIRECTORY = "sessions";
public static final String PREKEY_DIRECTORY = "prekeys"; protected static final String SESSIONS_DIRECTORY_V2 = "sessions-v2";
public static final String PREKEY_DIRECTORY = "prekeys";
protected final String address; protected final String address;
protected final String directory; protected final String directory;

View File

@ -0,0 +1,93 @@
package org.whispersystems.textsecure.storage;
import android.content.Context;
import android.util.Log;
import org.whispersystems.textsecure.crypto.IdentityKey;
import org.whispersystems.textsecure.crypto.MasterSecret;
/**
* Helper class for generating key pairs and calculating ECDH agreements.
*
* @author Moxie Marlinspike
*/
public class Session {
public static void clearV1SessionFor(Context context, CanonicalRecipientAddress recipient) {
//XXX Obviously we should probably do something more thorough here eventually.
LocalKeyRecord.delete(context, recipient);
RemoteKeyRecord.delete(context, recipient);
SessionRecordV1.delete(context, recipient);
}
public static void abortSessionFor(Context context, CanonicalRecipientAddress recipient) {
Log.w("Session", "Aborting session, deleting keys...");
clearV1SessionFor(context, recipient);
SessionRecordV2.delete(context, recipient);
}
public static boolean hasSession(Context context, MasterSecret masterSecret,
CanonicalRecipientAddress recipient)
{
Log.w("Session", "Checking session...");
return hasV1Session(context, recipient) || hasV2Session(context, masterSecret, recipient);
}
public static boolean hasRemoteIdentityKey(Context context,
MasterSecret masterSecret,
CanonicalRecipientAddress recipient)
{
return (hasV2Session(context, masterSecret, recipient) ||
(hasV1Session(context, recipient) &&
new SessionRecordV1(context, masterSecret, recipient).getIdentityKey() != null));
}
private static boolean hasV2Session(Context context, MasterSecret masterSecret,
CanonicalRecipientAddress recipient)
{
return SessionRecordV2.hasSession(context, masterSecret, recipient);
}
private static boolean hasV1Session(Context context, CanonicalRecipientAddress recipient) {
return SessionRecordV1.hasSession(context, recipient) &&
RemoteKeyRecord.hasRecord(context, recipient) &&
LocalKeyRecord.hasRecord(context, recipient);
}
public static IdentityKey getRemoteIdentityKey(Context context, MasterSecret masterSecret,
CanonicalRecipientAddress recipient)
{
if (SessionRecordV2.hasSession(context, masterSecret, recipient)) {
return new SessionRecordV2(context, masterSecret, recipient).getRemoteIdentityKey();
} else if (SessionRecordV1.hasSession(context, recipient)) {
return new SessionRecordV1(context, masterSecret, recipient).getIdentityKey();
} else {
return null;
}
}
public static IdentityKey getRemoteIdentityKey(Context context, MasterSecret masterSecret,
long recipientId)
{
if (SessionRecordV2.hasSession(context, masterSecret, recipientId)) {
return new SessionRecordV2(context, masterSecret, recipientId).getRemoteIdentityKey();
} else if (SessionRecordV1.hasSession(context, recipientId)) {
return new SessionRecordV1(context, masterSecret, recipientId).getIdentityKey();
} else {
return null;
}
}
public static int getSessionVersion(Context context, MasterSecret masterSecret,
CanonicalRecipientAddress recipient)
{
if (SessionRecordV2.hasSession(context, masterSecret, recipient)) {
return new SessionRecordV2(context, masterSecret, recipient).getSessionVersion();
} else if (SessionRecordV1.hasSession(context, recipient)) {
return new SessionRecordV1(context, masterSecret, recipient).getSessionVersion();
}
return 0;
}
}

View File

@ -19,7 +19,7 @@ package org.whispersystems.textsecure.storage;
import org.whispersystems.textsecure.crypto.InvalidMessageException; import org.whispersystems.textsecure.crypto.InvalidMessageException;
import org.whispersystems.textsecure.crypto.MasterCipher; import org.whispersystems.textsecure.crypto.MasterCipher;
import org.whispersystems.textsecure.crypto.MasterSecret; import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.crypto.SessionCipher; import org.whispersystems.textsecure.crypto.kdf.NKDF;
import org.whispersystems.textsecure.util.Conversions; import org.whispersystems.textsecure.util.Conversions;
import org.whispersystems.textsecure.util.Util; import org.whispersystems.textsecure.util.Util;
@ -77,13 +77,13 @@ public class SessionKey {
this.localKeyId = Conversions.byteArrayToMedium(decrypted, 0); this.localKeyId = Conversions.byteArrayToMedium(decrypted, 0);
this.remoteKeyId = Conversions.byteArrayToMedium(decrypted, 3); this.remoteKeyId = Conversions.byteArrayToMedium(decrypted, 3);
byte[] keyBytes = new byte[SessionCipher.CIPHER_KEY_LENGTH]; byte[] keyBytes = new byte[NKDF.LEGACY_CIPHER_KEY_LENGTH];
System.arraycopy(decrypted, 6, keyBytes, 0, keyBytes.length); System.arraycopy(decrypted, 6, keyBytes, 0, keyBytes.length);
byte[] macBytes = new byte[SessionCipher.MAC_KEY_LENGTH]; byte[] macBytes = new byte[NKDF.LEGACY_MAC_KEY_LENGTH];
System.arraycopy(decrypted, 6 + keyBytes.length, macBytes, 0, macBytes.length); System.arraycopy(decrypted, 6 + keyBytes.length, macBytes, 0, macBytes.length);
if (decrypted.length < 6 + SessionCipher.CIPHER_KEY_LENGTH + SessionCipher.MAC_KEY_LENGTH + 1) { if (decrypted.length < 6 + NKDF.LEGACY_CIPHER_KEY_LENGTH + NKDF.LEGACY_MAC_KEY_LENGTH + 1) {
throw new InvalidMessageException("No mode included"); throw new InvalidMessageException("No mode included");
} }

View File

@ -1,19 +1,3 @@
/**
* Copyright (C) 2011 Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.storage; package org.whispersystems.textsecure.storage;
import android.content.Context; import android.content.Context;
@ -36,30 +20,28 @@ import java.nio.channels.FileChannel;
* @author Moxie Marlinspike * @author Moxie Marlinspike
*/ */
public class SessionRecord extends Record { public class SessionRecordV1 extends Record {
private static final int CURRENT_VERSION_MARKER = 0X55555557; private static final int CURRENT_VERSION_MARKER = 0X55555556;
private static final int[] VALID_VERSION_MARKERS = {CURRENT_VERSION_MARKER, 0X55555556, 0X55555555}; private static final int[] VALID_VERSION_MARKERS = {CURRENT_VERSION_MARKER, 0X55555555};
private static final Object FILE_LOCK = new Object(); private static final Object FILE_LOCK = new Object();
private int counter; private int counter;
private byte[] localFingerprint; private byte[] localFingerprint;
private byte[] remoteFingerprint; private byte[] remoteFingerprint;
private int negotiatedSessionVersion;
private int currentSessionVersion; private int currentSessionVersion;
private IdentityKey identityKey; private IdentityKey identityKey;
private SessionKey sessionKeyRecord; private SessionKey sessionKeyRecord;
private boolean verifiedSessionKey; private boolean verifiedSessionKey;
private boolean prekeyBundleRequired;
private final MasterSecret masterSecret; private final MasterSecret masterSecret;
public SessionRecord(Context context, MasterSecret masterSecret, CanonicalRecipientAddress recipient) { public SessionRecordV1(Context context, MasterSecret masterSecret, CanonicalRecipientAddress recipient) {
this(context, masterSecret, getRecipientId(context, recipient)); this(context, masterSecret, getRecipientId(context, recipient));
} }
public SessionRecord(Context context, MasterSecret masterSecret, long recipientId) { public SessionRecordV1(Context context, MasterSecret masterSecret, long recipientId) {
super(context, SESSIONS_DIRECTORY, recipientId+""); super(context, SESSIONS_DIRECTORY, recipientId+"");
this.masterSecret = masterSecret; this.masterSecret = masterSecret;
this.currentSessionVersion = 31337; this.currentSessionVersion = 31337;
@ -71,8 +53,12 @@ public class SessionRecord extends Record {
} }
public static boolean hasSession(Context context, CanonicalRecipientAddress recipient) { public static boolean hasSession(Context context, CanonicalRecipientAddress recipient) {
Log.w("LocalKeyRecord", "Checking: " + getRecipientId(context, recipient)); return hasSession(context, getRecipientId(context, recipient));
return hasRecord(context, SESSIONS_DIRECTORY, getRecipientId(context, recipient)+""); }
public static boolean hasSession(Context context, long recipientId) {
Log.w("SessionRecordV1", "Checking: " + recipientId);
return hasRecord(context, SESSIONS_DIRECTORY, recipientId+"");
} }
private static long getRecipientId(Context context, CanonicalRecipientAddress recipient) { private static long getRecipientId(Context context, CanonicalRecipientAddress recipient) {
@ -96,14 +82,6 @@ public class SessionRecord extends Record {
return (currentSessionVersion == 31337 ? 0 : currentSessionVersion); return (currentSessionVersion == 31337 ? 0 : currentSessionVersion);
} }
public int getNegotiatedSessionVersion() {
return negotiatedSessionVersion;
}
public void setNegotiatedSessionVersion(int sessionVersion) {
this.negotiatedSessionVersion = sessionVersion;
}
public void setSessionVersion(int sessionVersion) { public void setSessionVersion(int sessionVersion) {
this.currentSessionVersion = sessionVersion; this.currentSessionVersion = sessionVersion;
} }
@ -128,18 +106,6 @@ public class SessionRecord extends Record {
return this.identityKey; return this.identityKey;
} }
public boolean isPrekeyBundleRequired() {
return prekeyBundleRequired;
}
public void setPrekeyBundleRequired(boolean prekeyBundleRequired) {
this.prekeyBundleRequired = prekeyBundleRequired;
}
// public void setVerifiedSessionKey(boolean verifiedSessionKey) {
// this.verifiedSessionKey = verifiedSessionKey;
// }
public boolean isVerifiedSession() { public boolean isVerifiedSession() {
return this.verifiedSessionKey; return this.verifiedSessionKey;
} }
@ -182,8 +148,6 @@ public class SessionRecord extends Record {
writeInteger(currentSessionVersion, out); writeInteger(currentSessionVersion, out);
writeIdentityKey(out); writeIdentityKey(out);
writeInteger(verifiedSessionKey ? 1 : 0, out); writeInteger(verifiedSessionKey ? 1 : 0, out);
writeInteger(prekeyBundleRequired ? 1 : 0, out);
writeInteger(negotiatedSessionVersion, out);
if (sessionKeyRecord != null) if (sessionKeyRecord != null)
writeBlob(sessionKeyRecord.serialize(), out); writeBlob(sessionKeyRecord.serialize(), out);
@ -230,13 +194,6 @@ public class SessionRecord extends Record {
this.verifiedSessionKey = (readInteger(in) == 1); this.verifiedSessionKey = (readInteger(in) == 1);
} }
if (versionMarker >= 0X55555557) {
this.prekeyBundleRequired = (readInteger(in) == 1);
this.negotiatedSessionVersion = readInteger(in);
} else {
this.negotiatedSessionVersion = currentSessionVersion;
}
if (in.available() != 0) { if (in.available() != 0) {
try { try {
this.sessionKeyRecord = new SessionKey(readBlob(in), masterSecret); this.sessionKeyRecord = new SessionKey(readBlob(in), masterSecret);
@ -265,7 +222,7 @@ public class SessionRecord extends Record {
(this.sessionKeyRecord.getRemoteKeyId() == remoteKeyId) && (this.sessionKeyRecord.getRemoteKeyId() == remoteKeyId) &&
(this.sessionKeyRecord.getMode() == mode)) (this.sessionKeyRecord.getMode() == mode))
{ {
return this.sessionKeyRecord; return this.sessionKeyRecord;
} }
return null; return null;

View File

@ -0,0 +1,505 @@
/**
* Copyright (C) 2013 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.storage;
import android.content.Context;
import android.util.Log;
import android.util.Pair;
import com.google.protobuf.ByteString;
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.MasterCipher;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.crypto.ecc.Curve;
import org.whispersystems.textsecure.crypto.ecc.ECKeyPair;
import org.whispersystems.textsecure.crypto.ecc.ECPrivateKey;
import org.whispersystems.textsecure.crypto.ecc.ECPublicKey;
import org.whispersystems.textsecure.crypto.ratchet.ChainKey;
import org.whispersystems.textsecure.crypto.ratchet.MessageKeys;
import org.whispersystems.textsecure.crypto.ratchet.RootKey;
import org.whispersystems.textsecure.storage.StorageProtos.SessionStructure.Chain;
import org.whispersystems.textsecure.storage.StorageProtos.SessionStructure.PendingKeyExchange;
import org.whispersystems.textsecure.storage.StorageProtos.SessionStructure.PendingPreKey;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.util.Iterator;
import java.util.List;
import javax.crypto.spec.SecretKeySpec;
/**
* A disk record representing a current session.
*
* @author Moxie Marlinspike
*/
public class SessionRecordV2 extends Record {
private static final Object FILE_LOCK = new Object();
private static final int CURRENT_VERSION = 1;
private final MasterSecret masterSecret;
private StorageProtos.SessionStructure sessionStructure =
StorageProtos.SessionStructure.newBuilder().build();
public SessionRecordV2(Context context, MasterSecret masterSecret, CanonicalRecipientAddress recipient) {
this(context, masterSecret, getRecipientId(context, recipient));
}
public SessionRecordV2(Context context, MasterSecret masterSecret, long recipientId) {
super(context, SESSIONS_DIRECTORY_V2, recipientId+"");
this.masterSecret = masterSecret;
loadData();
}
public static void delete(Context context, CanonicalRecipientAddress recipient) {
delete(context, SESSIONS_DIRECTORY_V2, getRecipientId(context, recipient) + "");
}
public static boolean hasSession(Context context, MasterSecret masterSecret,
CanonicalRecipientAddress recipient)
{
return hasSession(context, masterSecret, getRecipientId(context, recipient));
}
public static boolean hasSession(Context context, MasterSecret masterSecret, long recipientId) {
return hasRecord(context, SESSIONS_DIRECTORY_V2, recipientId+"") &&
new SessionRecordV2(context, masterSecret, recipientId).hasSenderChain();
}
private static long getRecipientId(Context context, CanonicalRecipientAddress recipient) {
return recipient.getCanonicalAddress(context);
}
public void clear() {
this.sessionStructure = StorageProtos.SessionStructure.newBuilder().build();
}
public void setSessionVersion(int version) {
this.sessionStructure = this.sessionStructure.toBuilder()
.setSessionVersion(version)
.build();
}
public int getSessionVersion() {
return this.sessionStructure.getSessionVersion();
}
public void setRemoteIdentityKey(IdentityKey identityKey) {
this.sessionStructure = this.sessionStructure.toBuilder()
.setRemoteIdentityPublic(ByteString.copyFrom(identityKey.serialize()))
.build();
}
public void setLocalIdentityKey(IdentityKey identityKey) {
this.sessionStructure = this.sessionStructure.toBuilder()
.setLocalIdentityPublic(ByteString.copyFrom(identityKey.serialize()))
.build();
}
public IdentityKey getRemoteIdentityKey() {
try {
if (!this.sessionStructure.hasRemoteIdentityPublic()) {
return null;
}
return new IdentityKey(this.sessionStructure.getRemoteIdentityPublic().toByteArray(), 0);
} catch (InvalidKeyException e) {
Log.w("SessionRecordV2", e);
return null;
}
}
public IdentityKey getLocalIdentityKey() {
try {
return new IdentityKey(this.sessionStructure.getLocalIdentityPublic().toByteArray(), 0);
} catch (InvalidKeyException e) {
throw new AssertionError(e);
}
}
public int getPreviousCounter() {
return sessionStructure.getPreviousCounter();
}
public void setPreviousCounter(int previousCounter) {
this.sessionStructure = this.sessionStructure.toBuilder()
.setPreviousCounter(previousCounter)
.build();
}
public RootKey getRootKey() {
return new RootKey(this.sessionStructure.getRootKey().toByteArray());
}
public void setRootKey(RootKey rootKey) {
this.sessionStructure = this.sessionStructure.toBuilder()
.setRootKey(ByteString.copyFrom(rootKey.getKeyBytes()))
.build();
}
public ECPublicKey getSenderEphemeral() {
try {
return Curve.decodePoint(sessionStructure.getSenderChain().getSenderEphemeral().toByteArray(), 0);
} catch (InvalidKeyException e) {
throw new AssertionError(e);
}
}
public ECKeyPair getSenderEphemeralPair() {
ECPublicKey publicKey = getSenderEphemeral();
ECPrivateKey privateKey = Curve.decodePrivatePoint(publicKey.getType(),
sessionStructure.getSenderChain()
.getSenderEphemeralPrivate()
.toByteArray());
return new ECKeyPair(publicKey, privateKey);
}
public boolean hasReceiverChain(ECPublicKey senderEphemeral) {
return getReceiverChain(senderEphemeral) != null;
}
public boolean hasSenderChain() {
return sessionStructure.hasSenderChain();
}
private Pair<Chain,Integer> getReceiverChain(ECPublicKey senderEphemeral) {
List<Chain> receiverChains = sessionStructure.getReceiverChainsList();
int index = 0;
for (Chain receiverChain : receiverChains) {
try {
ECPublicKey chainSenderEphemeral = Curve.decodePoint(receiverChain.getSenderEphemeral().toByteArray(), 0);
if (chainSenderEphemeral.equals(senderEphemeral)) {
return new Pair<Chain,Integer>(receiverChain,index);
}
} catch (InvalidKeyException e) {
Log.w("SessionRecordV2", e);
}
index++;
}
return null;
}
public ChainKey getReceiverChainKey(ECPublicKey senderEphemeral) {
Pair<Chain,Integer> receiverChainAndIndex = getReceiverChain(senderEphemeral);
Chain receiverChain = receiverChainAndIndex.first;
if (receiverChain == null) {
return null;
} else {
return new ChainKey(receiverChain.getChainKey().getKey().toByteArray(),
receiverChain.getChainKey().getIndex());
}
}
public void addReceiverChain(ECPublicKey senderEphemeral, ChainKey chainKey) {
Chain.ChainKey chainKeyStructure = Chain.ChainKey.newBuilder()
.setKey(ByteString.copyFrom(chainKey.getKey()))
.setIndex(chainKey.getIndex())
.build();
Chain chain = Chain.newBuilder()
.setChainKey(chainKeyStructure)
.setSenderEphemeral(ByteString.copyFrom(senderEphemeral.serialize()))
.build();
// XXX knock old chain out.
this.sessionStructure = this.sessionStructure.toBuilder().addReceiverChains(chain).build();
}
public void setSenderChain(ECKeyPair senderEphemeralPair, ChainKey chainKey) {
Chain.ChainKey chainKeyStructure = Chain.ChainKey.newBuilder()
.setKey(ByteString.copyFrom(chainKey.getKey()))
.setIndex(chainKey.getIndex())
.build();
Chain senderChain = Chain.newBuilder()
.setSenderEphemeral(ByteString.copyFrom(senderEphemeralPair.getPublicKey().serialize()))
.setSenderEphemeralPrivate(ByteString.copyFrom(senderEphemeralPair.getPrivateKey().serialize()))
.setChainKey(chainKeyStructure)
.build();
this.sessionStructure = this.sessionStructure.toBuilder().setSenderChain(senderChain).build();
}
public ChainKey getSenderChainKey() {
Chain.ChainKey chainKeyStructure = sessionStructure.getSenderChain().getChainKey();
return new ChainKey(chainKeyStructure.getKey().toByteArray(), chainKeyStructure.getIndex());
}
public void setSenderChainKey(ChainKey nextChainKey) {
Chain.ChainKey chainKey = Chain.ChainKey.newBuilder()
.setKey(ByteString.copyFrom(nextChainKey.getKey()))
.setIndex(nextChainKey.getIndex())
.build();
Chain chain = sessionStructure.getSenderChain().toBuilder()
.setChainKey(chainKey).build();
this.sessionStructure = this.sessionStructure.toBuilder().setSenderChain(chain).build();
}
public boolean hasMessageKeys(ECPublicKey senderEphemeral, int counter) {
Pair<Chain,Integer> chainAndIndex = getReceiverChain(senderEphemeral);
Chain chain = chainAndIndex.first;
if (chain == null) {
return false;
}
List<Chain.MessageKey> messageKeyList = chain.getMessageKeysList();
for (Chain.MessageKey messageKey : messageKeyList) {
if (messageKey.getIndex() == counter) {
return true;
}
}
return false;
}
public MessageKeys removeMessageKeys(ECPublicKey senderEphemeral, int counter) {
Pair<Chain,Integer> chainAndIndex = getReceiverChain(senderEphemeral);
Chain chain = chainAndIndex.first;
if (chain == null) {
return null;
}
List<Chain.MessageKey> messageKeyList = chain.getMessageKeysList();
Iterator<Chain.MessageKey> messageKeyIterator = messageKeyList.iterator();
MessageKeys result = null;
while (messageKeyIterator.hasNext()) {
Chain.MessageKey messageKey = messageKeyIterator.next();
if (messageKey.getIndex() == counter) {
result = new MessageKeys(new SecretKeySpec(messageKey.getCipherKey().toByteArray(), "AES"),
new SecretKeySpec(messageKey.getMacKey().toByteArray(), "HmacSHA256"),
messageKey.getIndex());
messageKeyIterator.remove();
break;
}
}
Chain updatedChain = chain.toBuilder().clearMessageKeys()
.addAllMessageKeys(messageKeyList)
.build();
this.sessionStructure = this.sessionStructure.toBuilder()
.setReceiverChains(chainAndIndex.second, updatedChain)
.build();
return result;
}
public void setMessageKeys(ECPublicKey senderEphemeral, MessageKeys messageKeys) {
Pair<Chain,Integer> chainAndIndex = getReceiverChain(senderEphemeral);
Chain chain = chainAndIndex.first;
Chain.MessageKey messageKeyStructure = Chain.MessageKey.newBuilder()
.setCipherKey(ByteString.copyFrom(messageKeys.getCipherKey().getEncoded()))
.setMacKey(ByteString.copyFrom(messageKeys.getMacKey().getEncoded()))
.setIndex(messageKeys.getCounter())
.build();
Chain updatedChain = chain.toBuilder()
.addMessageKeys(messageKeyStructure)
.build();
this.sessionStructure = this.sessionStructure.toBuilder()
.setReceiverChains(chainAndIndex.second, updatedChain)
.build();
}
public void setReceiverChainKey(ECPublicKey senderEphemeral, ChainKey chainKey) {
Pair<Chain,Integer> chainAndIndex = getReceiverChain(senderEphemeral);
Chain chain = chainAndIndex.first;
Chain.ChainKey chainKeyStructure = Chain.ChainKey.newBuilder()
.setKey(ByteString.copyFrom(chainKey.getKey()))
.setIndex(chainKey.getIndex())
.build();
Chain updatedChain = chain.toBuilder().setChainKey(chainKeyStructure).build();
this.sessionStructure = this.sessionStructure.toBuilder()
.setReceiverChains(chainAndIndex.second, updatedChain)
.build();
}
public void setPendingKeyExchange(int sequence,
ECKeyPair ourBaseKey,
ECKeyPair ourEphemeralKey,
IdentityKeyPair ourIdentityKey)
{
PendingKeyExchange structure =
PendingKeyExchange.newBuilder()
.setSequence(sequence)
.setLocalBaseKey(ByteString.copyFrom(ourBaseKey.getPublicKey().serialize()))
.setLocalBaseKeyPrivate(ByteString.copyFrom(ourBaseKey.getPrivateKey().serialize()))
.setLocalEphemeralKey(ByteString.copyFrom(ourEphemeralKey.getPublicKey().serialize()))
.setLocalEphemeralKeyPrivate(ByteString.copyFrom(ourEphemeralKey.getPrivateKey().serialize()))
.setLocalIdentityKey(ByteString.copyFrom(ourIdentityKey.getPublicKey().serialize()))
.setLocalIdentityKeyPrivate(ByteString.copyFrom(ourIdentityKey.getPrivateKey().serialize()))
.build();
this.sessionStructure = this.sessionStructure.toBuilder()
.setPendingKeyExchange(structure)
.build();
}
public int getPendingKeyExchangeSequence() {
return sessionStructure.getPendingKeyExchange().getSequence();
}
public ECKeyPair getPendingKeyExchangeBaseKey() throws InvalidKeyException {
ECPublicKey publicKey = Curve.decodePoint(sessionStructure.getPendingKeyExchange()
.getLocalBaseKey().toByteArray(), 0);
ECPrivateKey privateKey = Curve.decodePrivatePoint(publicKey.getType(),
sessionStructure.getPendingKeyExchange()
.getLocalBaseKeyPrivate()
.toByteArray());
return new ECKeyPair(publicKey, privateKey);
}
public ECKeyPair getPendingKeyExchangeEphemeralKey() throws InvalidKeyException {
ECPublicKey publicKey = Curve.decodePoint(sessionStructure.getPendingKeyExchange()
.getLocalEphemeralKey().toByteArray(), 0);
ECPrivateKey privateKey = Curve.decodePrivatePoint(publicKey.getType(),
sessionStructure.getPendingKeyExchange()
.getLocalEphemeralKeyPrivate()
.toByteArray());
return new ECKeyPair(publicKey, privateKey);
}
public IdentityKeyPair getPendingKeyExchangeIdentityKey() throws InvalidKeyException {
IdentityKey publicKey = new IdentityKey(sessionStructure.getPendingKeyExchange()
.getLocalIdentityKey().toByteArray(), 0);
ECPrivateKey privateKey = Curve.decodePrivatePoint(publicKey.getPublicKey().getType(),
sessionStructure.getPendingKeyExchange()
.getLocalIdentityKeyPrivate()
.toByteArray());
return new IdentityKeyPair(publicKey, privateKey);
}
public boolean hasPendingKeyExchange() {
return sessionStructure.hasPendingKeyExchange();
}
public void setPendingPreKey(int preKeyId, ECPublicKey baseKey) {
PendingPreKey pending = PendingPreKey.newBuilder()
.setPreKeyId(preKeyId)
.setBaseKey(ByteString.copyFrom(baseKey.serialize()))
.build();
this.sessionStructure = this.sessionStructure.toBuilder()
.setPendingPreKey(pending)
.build();
}
public boolean hasPendingPreKey() {
return this.sessionStructure.hasPendingPreKey();
}
public Pair<Integer, ECPublicKey> getPendingPreKey() {
try {
return new Pair<Integer, ECPublicKey>(sessionStructure.getPendingPreKey().getPreKeyId(),
Curve.decodePoint(sessionStructure.getPendingPreKey()
.getBaseKey()
.toByteArray(), 0));
} catch (InvalidKeyException e) {
throw new AssertionError(e);
}
}
public void clearPendingPreKey() {
this.sessionStructure = this.sessionStructure.toBuilder()
.clearPendingPreKey()
.build();
}
public void save() {
synchronized (FILE_LOCK) {
try {
RandomAccessFile file = openRandomAccessFile();
FileChannel out = file.getChannel();
out.position(0);
MasterCipher cipher = new MasterCipher(masterSecret);
writeInteger(CURRENT_VERSION, out);
writeBlob(cipher.encryptBytes(sessionStructure.toByteArray()), out);
out.truncate(out.position());
file.close();
} catch (IOException ioe) {
throw new IllegalArgumentException(ioe);
}
}
}
private void loadData() {
synchronized (FILE_LOCK) {
try {
FileInputStream in = this.openInputStream();
int versionMarker = readInteger(in);
if (versionMarker > CURRENT_VERSION) {
throw new AssertionError("Unknown version: " + versionMarker);
}
MasterCipher cipher = new MasterCipher(masterSecret);
byte[] encryptedBlob = readBlob(in);
this.sessionStructure = StorageProtos.SessionStructure
.parseFrom(cipher.decryptBytes(encryptedBlob));
in.close();
} catch (FileNotFoundException e) {
Log.w("SessionRecordV2", "No session information found.");
// XXX
} catch (IOException ioe) {
Log.w("SessionRecordV2", ioe);
// XXX
} catch (InvalidMessageException e) {
Log.w("SessionRecordV2", e);
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -5,8 +5,9 @@
android:icon="@drawable/ic_menu_lock_holo_dark" android:icon="@drawable/ic_menu_lock_holo_dark"
android:showAsAction="ifRoom"> android:showAsAction="ifRoom">
<menu> <menu>
<item android:title="@string/conversation_secure_verified__menu_verify_session" <item android:title="@string/conversation_secure_verified__menu_no_identity"
android:id="@+id/menu_verify_session" /> android:enabled="false"
android:id="@+id/menu_no_identity" />
<item android:title="@string/conversation_secure_verified__menu_abort_secure_session" <item android:title="@string/conversation_secure_verified__menu_abort_secure_session"
android:id="@+id/menu_abort_session"/> android:id="@+id/menu_abort_session"/>

View File

@ -688,7 +688,7 @@
<!-- conversation_secure_verified --> <!-- conversation_secure_verified -->
<string name="conversation_secure_verified__menu_security">Security</string> <string name="conversation_secure_verified__menu_security">Security</string>
<string name="conversation_secure_verified__menu_verify_session">Verify Session</string> <string name="conversation_secure_verified__menu_no_identity">No Identity Available</string>
<string name="conversation_secure_verified__menu_verify_recipient">Verify Recipient</string> <string name="conversation_secure_verified__menu_verify_recipient">Verify Recipient</string>
<string name="conversation_secure_verified__menu_abort_secure_session">Abort Secure Session</string> <string name="conversation_secure_verified__menu_abort_secure_session">Abort Secure Session</string>

View File

@ -96,7 +96,6 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredSherlockPr
initializeIdentitySelection(); initializeIdentitySelection();
initializePlatformSpecificOptions(); initializePlatformSpecificOptions();
initializePushMessagingToggle(); initializePushMessagingToggle();
initializeEditTextSummaries();
this.findPreference(TextSecurePreferences.CHANGE_PASSPHRASE_PREF) this.findPreference(TextSecurePreferences.CHANGE_PASSPHRASE_PREF)
.setOnPreferenceClickListener(new ChangePassphraseClickListener()); .setOnPreferenceClickListener(new ChangePassphraseClickListener());
@ -210,12 +209,6 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredSherlockPr
}); });
} }
private void initializeEditTextSummaries() {
initializeEditTextSummary((EditTextPreference)this.findPreference(TextSecurePreferences.MMSC_HOST_PREF));
initializeEditTextSummary((EditTextPreference)this.findPreference(TextSecurePreferences.MMSC_PROXY_HOST_PREF));
initializeEditTextSummary((EditTextPreference)this.findPreference(TextSecurePreferences.MMSC_PROXY_PORT_PREF));
}
private void initializePushMessagingToggle() { private void initializePushMessagingToggle() {
CheckBoxPreference preference = (CheckBoxPreference)this.findPreference(PUSH_MESSAGING_PREF); CheckBoxPreference preference = (CheckBoxPreference)this.findPreference(PUSH_MESSAGING_PREF);
preference.setChecked(TextSecurePreferences.isPushRegistered(this)); preference.setChecked(TextSecurePreferences.isPushRegistered(this));

View File

@ -51,18 +51,14 @@ import android.widget.Toast;
import com.actionbarsherlock.view.Menu; import com.actionbarsherlock.view.Menu;
import com.actionbarsherlock.view.MenuInflater; import com.actionbarsherlock.view.MenuInflater;
import com.actionbarsherlock.view.MenuItem; import com.actionbarsherlock.view.MenuItem;
import org.thoughtcrime.securesms.components.EmojiDrawer; import org.thoughtcrime.securesms.components.EmojiDrawer;
import org.thoughtcrime.securesms.components.EmojiToggle; import org.thoughtcrime.securesms.components.EmojiToggle;
import org.thoughtcrime.securesms.components.RecipientsPanel; import org.thoughtcrime.securesms.components.RecipientsPanel;
import org.thoughtcrime.securesms.contacts.ContactAccessor; import org.thoughtcrime.securesms.contacts.ContactAccessor;
import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData; import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData;
import org.thoughtcrime.securesms.contacts.ContactAccessor.NumberData;
import org.thoughtcrime.securesms.crypto.KeyExchangeInitiator; import org.thoughtcrime.securesms.crypto.KeyExchangeInitiator;
import org.thoughtcrime.securesms.crypto.KeyExchangeProcessor; import org.thoughtcrime.securesms.crypto.KeyExchangeProcessor;
import org.whispersystems.textsecure.crypto.InvalidMessageException;
import org.whispersystems.textsecure.crypto.KeyUtil;
import org.whispersystems.textsecure.crypto.MasterCipher;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.DraftDatabase; import org.thoughtcrime.securesms.database.DraftDatabase;
import org.thoughtcrime.securesms.database.DraftDatabase.Draft; import org.thoughtcrime.securesms.database.DraftDatabase.Draft;
@ -89,6 +85,10 @@ import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.EncryptedCharacterCalculator; import org.thoughtcrime.securesms.util.EncryptedCharacterCalculator;
import org.thoughtcrime.securesms.util.MemoryCleaner; import org.thoughtcrime.securesms.util.MemoryCleaner;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.textsecure.crypto.InvalidMessageException;
import org.whispersystems.textsecure.crypto.MasterCipher;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.storage.Session;
import org.whispersystems.textsecure.util.Util; import org.whispersystems.textsecure.util.Util;
import java.io.IOException; import java.io.IOException;
@ -275,7 +275,6 @@ public class ConversationActivity extends PassphraseRequiredSherlockFragmentActi
case R.id.menu_start_secure_session: handleStartSecureSession(); return true; case R.id.menu_start_secure_session: handleStartSecureSession(); return true;
case R.id.menu_abort_session: handleAbortSecureSession(); return true; case R.id.menu_abort_session: handleAbortSecureSession(); return true;
case R.id.menu_verify_recipient: handleVerifyRecipient(); return true; case R.id.menu_verify_recipient: handleVerifyRecipient(); return true;
case R.id.menu_verify_session: handleVerifySession(); return true;
case R.id.menu_group_recipients: handleDisplayGroupRecipients(); return true; case R.id.menu_group_recipients: handleDisplayGroupRecipients(); return true;
case R.id.menu_distribution_broadcast: handleDistributionBroadcastEnabled(item); return true; case R.id.menu_distribution_broadcast: handleDistributionBroadcastEnabled(item); return true;
case R.id.menu_distribution_conversation: handleDistributionConversationEnabled(item); return true; case R.id.menu_distribution_conversation: handleDistributionConversationEnabled(item); return true;
@ -329,13 +328,6 @@ public class ConversationActivity extends PassphraseRequiredSherlockFragmentActi
startActivity(verifyIdentityIntent); startActivity(verifyIdentityIntent);
} }
private void handleVerifySession() {
Intent verifyKeysIntent = new Intent(this, VerifyKeysActivity.class);
verifyKeysIntent.putExtra("recipient", getRecipients().getPrimaryRecipient());
verifyKeysIntent.putExtra("master_secret", masterSecret);
startActivity(verifyKeysIntent);
}
private void handleStartSecureSession() { private void handleStartSecureSession() {
if (getRecipients() == null) { if (getRecipients() == null) {
Toast.makeText(this, getString(R.string.ConversationActivity_invalid_recipient), Toast.makeText(this, getString(R.string.ConversationActivity_invalid_recipient),
@ -373,7 +365,7 @@ public class ConversationActivity extends PassphraseRequiredSherlockFragmentActi
@Override @Override
public void onClick(DialogInterface dialog, int which) { public void onClick(DialogInterface dialog, int which) {
if (isSingleConversation()) { if (isSingleConversation()) {
KeyUtil.abortSessionFor(ConversationActivity.this, getRecipients().getPrimaryRecipient()); Session.abortSessionFor(ConversationActivity.this, getRecipients().getPrimaryRecipient());
initializeSecurity(); initializeSecurity();
initializeTitleBar(); initializeTitleBar();
} }
@ -567,11 +559,11 @@ public class ConversationActivity extends PassphraseRequiredSherlockFragmentActi
TypedArray drawables = obtainStyledAttributes(attributes); TypedArray drawables = obtainStyledAttributes(attributes);
if (isSingleConversation() && if (isSingleConversation() &&
KeyUtil.isSessionFor(this, getRecipients().getPrimaryRecipient())) Session.hasSession(this, masterSecret, getRecipients().getPrimaryRecipient()))
{ {
sendButton.setImageDrawable(drawables.getDrawable(1)); sendButton.setImageDrawable(drawables.getDrawable(1));
this.isEncryptedConversation = true; this.isEncryptedConversation = true;
this.isAuthenticatedConversation = KeyUtil.isIdentityKeyFor(this, masterSecret, getRecipients().getPrimaryRecipient()); this.isAuthenticatedConversation = Session.hasRemoteIdentityKey(this, masterSecret, getRecipients().getPrimaryRecipient());
this.characterCalculator = new EncryptedCharacterCalculator(); this.characterCalculator = new EncryptedCharacterCalculator();
} else { } else {
sendButton.setImageDrawable(drawables.getDrawable(0)); sendButton.setImageDrawable(drawables.getDrawable(0));

View File

@ -31,17 +31,19 @@ import android.widget.Button;
import android.widget.TextView; import android.widget.TextView;
import org.thoughtcrime.securesms.crypto.DecryptingQueue; import org.thoughtcrime.securesms.crypto.DecryptingQueue;
import org.thoughtcrime.securesms.sms.SmsTransportDetails;
import org.whispersystems.textsecure.crypto.InvalidKeyException;
import org.whispersystems.textsecure.crypto.InvalidVersionException;
import org.thoughtcrime.securesms.crypto.protocol.KeyExchangeMessage;
import org.thoughtcrime.securesms.crypto.KeyExchangeProcessor; import org.thoughtcrime.securesms.crypto.KeyExchangeProcessor;
import org.whispersystems.textsecure.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.KeyExchangeProcessorV2;
import org.thoughtcrime.securesms.crypto.protocol.KeyExchangeMessage;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.sms.SmsTransportDetails;
import org.thoughtcrime.securesms.util.MemoryCleaner; import org.thoughtcrime.securesms.util.MemoryCleaner;
import org.whispersystems.textsecure.crypto.InvalidKeyException;
import org.whispersystems.textsecure.crypto.InvalidMessageException;
import org.whispersystems.textsecure.crypto.InvalidVersionException;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage; import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage;
import org.whispersystems.textsecure.crypto.protocol.PreKeyBundleMessage; import org.whispersystems.textsecure.crypto.protocol.PreKeyWhisperMessage;
import org.whispersystems.textsecure.storage.InvalidKeyIdException; import org.whispersystems.textsecure.storage.InvalidKeyIdException;
import java.io.IOException; import java.io.IOException;
@ -64,9 +66,8 @@ public class ReceiveKeyActivity extends Activity {
private long messageId; private long messageId;
private MasterSecret masterSecret; private MasterSecret masterSecret;
private PreKeyBundleMessage keyExchangeMessageBundle; private PreKeyWhisperMessage keyExchangeMessageBundle;
private KeyExchangeMessage keyExchangeMessage; private KeyExchangeMessage keyExchangeMessage;
private KeyExchangeProcessor keyExchangeProcessor;
@Override @Override
protected void onCreate(Bundle state) { protected void onCreate(Bundle state) {
@ -82,6 +83,8 @@ public class ReceiveKeyActivity extends Activity {
Log.w("ReceiveKeyActivity", ike); Log.w("ReceiveKeyActivity", ike);
} catch (InvalidVersionException ive) { } catch (InvalidVersionException ive) {
Log.w("ReceiveKeyActivity", ive); Log.w("ReceiveKeyActivity", ive);
} catch (InvalidMessageException e) {
Log.w("ReceiveKeyActivity", e);
} }
initializeListeners(); initializeListeners();
} }
@ -122,12 +125,22 @@ public class ReceiveKeyActivity extends Activity {
descriptionText.setMovementMethod(LinkMovementMethod.getInstance()); descriptionText.setMovementMethod(LinkMovementMethod.getInstance());
} }
private boolean isTrusted(KeyExchangeMessage message, PreKeyBundleMessage messageBundle) { private boolean isTrusted(KeyExchangeMessage message, PreKeyWhisperMessage messageBundle) {
return (message != null && keyExchangeProcessor.isTrusted(message)) || if (message != null) {
(messageBundle != null && keyExchangeProcessor.isTrusted(messageBundle)); KeyExchangeProcessor processor = KeyExchangeProcessor.createFor(this, masterSecret,
recipient, message);
return processor.isTrusted(message);
} else if (messageBundle != null) {
KeyExchangeProcessorV2 processor = new KeyExchangeProcessorV2(this, masterSecret, recipient);
return processor.isTrusted(messageBundle);
}
return false;
} }
private void initializeKey() throws InvalidKeyException, InvalidVersionException { private void initializeKey()
throws InvalidKeyException, InvalidVersionException, InvalidMessageException
{
try { try {
String messageBody = getIntent().getStringExtra("body"); String messageBody = getIntent().getStringExtra("body");
@ -135,9 +148,9 @@ public class ReceiveKeyActivity extends Activity {
SmsTransportDetails transportDetails = new SmsTransportDetails(); SmsTransportDetails transportDetails = new SmsTransportDetails();
byte[] body = transportDetails.getDecodedMessage(messageBody.getBytes()); byte[] body = transportDetails.getDecodedMessage(messageBody.getBytes());
this.keyExchangeMessageBundle = new PreKeyBundleMessage(body); this.keyExchangeMessageBundle = new PreKeyWhisperMessage(body);
} else { } else {
this.keyExchangeMessage = new KeyExchangeMessage(messageBody); this.keyExchangeMessage = KeyExchangeMessage.createFor(messageBody);
} }
} catch (IOException e) { } catch (IOException e) {
throw new AssertionError(e); throw new AssertionError(e);
@ -152,7 +165,6 @@ public class ReceiveKeyActivity extends Activity {
this.threadId = getIntent().getLongExtra("thread_id", -1); this.threadId = getIntent().getLongExtra("thread_id", -1);
this.messageId = getIntent().getLongExtra("message_id", -1); this.messageId = getIntent().getLongExtra("message_id", -1);
this.masterSecret = getIntent().getParcelableExtra("master_secret"); this.masterSecret = getIntent().getParcelableExtra("master_secret");
this.keyExchangeProcessor = new KeyExchangeProcessor(this, masterSecret, recipient);
} }
private void initializeListeners() { private void initializeListeners() {
@ -177,13 +189,23 @@ public class ReceiveKeyActivity extends Activity {
@Override @Override
protected Void doInBackground(Void... params) { protected Void doInBackground(Void... params) {
if (keyExchangeMessage != null) { if (keyExchangeMessage != null) {
keyExchangeProcessor.processKeyExchangeMessage(keyExchangeMessage, threadId); try {
DatabaseFactory.getEncryptingSmsDatabase(ReceiveKeyActivity.this) KeyExchangeProcessor processor = KeyExchangeProcessor.createFor(ReceiveKeyActivity.this, masterSecret, recipient, keyExchangeMessage);
.markAsProcessedKeyExchange(messageId); processor.processKeyExchangeMessage(keyExchangeMessage, threadId);
DatabaseFactory.getEncryptingSmsDatabase(ReceiveKeyActivity.this)
.markAsProcessedKeyExchange(messageId);
} catch (InvalidMessageException e) {
Log.w("ReceiveKeyActivity", e);
DatabaseFactory.getEncryptingSmsDatabase(ReceiveKeyActivity.this)
.markAsCorruptKeyExchange(messageId);
}
} else if (keyExchangeMessageBundle != null) { } else if (keyExchangeMessageBundle != null) {
try { try {
keyExchangeProcessor.processKeyExchangeMessage(keyExchangeMessageBundle); KeyExchangeProcessorV2 processor = new KeyExchangeProcessorV2(ReceiveKeyActivity.this,
CiphertextMessage bundledMessage = keyExchangeMessageBundle.getBundledMessage(); masterSecret, recipient);
processor.processKeyExchangeMessage(keyExchangeMessageBundle);
CiphertextMessage bundledMessage = keyExchangeMessageBundle.getWhisperMessage();
SmsTransportDetails transportDetails = new SmsTransportDetails(); SmsTransportDetails transportDetails = new SmsTransportDetails();
String messageBody = new String(transportDetails.getEncodedMessage(bundledMessage.serialize())); String messageBody = new String(transportDetails.getEncodedMessage(bundledMessage.serialize()));

View File

@ -21,15 +21,14 @@ import android.os.Bundle;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import org.whispersystems.textsecure.crypto.IdentityKey;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.crypto.ecc.Curve;
import org.whispersystems.textsecure.crypto.ecc.ECPublicKey;
import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage;
import org.whispersystems.textsecure.storage.SessionRecord;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.MemoryCleaner; import org.thoughtcrime.securesms.util.MemoryCleaner;
import org.whispersystems.textsecure.crypto.IdentityKey;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.crypto.ecc.Curve;
import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage;
import org.whispersystems.textsecure.storage.Session;
/** /**
* Activity for verifying identity keys. * Activity for verifying identity keys.
@ -68,15 +67,14 @@ public class VerifyIdentityActivity extends KeyScanningActivity {
return; return;
} }
localIdentityFingerprint.setText(IdentityKeyUtil.getFingerprint(this, keyType)); localIdentityFingerprint.setText(IdentityKeyUtil.getIdentityKey(this, keyType).getFingerprint());
} }
private void initializeRemoteIdentityKey() { private void initializeRemoteIdentityKey() {
IdentityKey identityKey = getIntent().getParcelableExtra("remote_identity"); IdentityKey identityKey = getIntent().getParcelableExtra("remote_identity");
if (identityKey == null) { if (identityKey == null) {
SessionRecord sessionRecord = new SessionRecord(this, masterSecret, recipient); identityKey = Session.getRemoteIdentityKey(this, masterSecret, recipient);
identityKey = sessionRecord.getIdentityKey();
} }
if (identityKey == null) { if (identityKey == null) {
@ -97,13 +95,12 @@ public class VerifyIdentityActivity extends KeyScanningActivity {
this.recipient = this.getIntent().getParcelableExtra("recipient"); this.recipient = this.getIntent().getParcelableExtra("recipient");
this.masterSecret = this.getIntent().getParcelableExtra("master_secret"); this.masterSecret = this.getIntent().getParcelableExtra("master_secret");
SessionRecord sessionRecord = new SessionRecord(this, masterSecret, recipient); int sessionVersion = Session.getSessionVersion(this, masterSecret, recipient);
int sessionVersion = sessionRecord.getSessionVersion();
if (sessionVersion >= CiphertextMessage.CURVE25519_INTRODUCED_VERSION) { if (sessionVersion <= CiphertextMessage.LEGACY_VERSION) {
this.keyType = Curve.DJB_TYPE;
} else {
this.keyType = Curve.NIST_TYPE; this.keyType = Curve.NIST_TYPE;
} else {
this.keyType = Curve.DJB_TYPE;
} }
} }
@ -121,8 +118,7 @@ public class VerifyIdentityActivity extends KeyScanningActivity {
@Override @Override
protected void initiateScan() { protected void initiateScan() {
SessionRecord sessionRecord = new SessionRecord(this, masterSecret, recipient); IdentityKey identityKey = Session.getRemoteIdentityKey(this, masterSecret, recipient);
IdentityKey identityKey = sessionRecord.getIdentityKey();
if (identityKey == null) { if (identityKey == null) {
Toast.makeText(this, R.string.VerifyIdentityActivity_recipient_has_no_identity_key_exclamation, Toast.makeText(this, R.string.VerifyIdentityActivity_recipient_has_no_identity_key_exclamation,
@ -144,8 +140,7 @@ public class VerifyIdentityActivity extends KeyScanningActivity {
@Override @Override
protected IdentityKey getIdentityKeyToCompare() { protected IdentityKey getIdentityKeyToCompare() {
SessionRecord sessionRecord = new SessionRecord(this, masterSecret, recipient); return Session.getRemoteIdentityKey(this, masterSecret, recipient);
return sessionRecord.getIdentityKey();
} }
@Override @Override

View File

@ -1,130 +0,0 @@
/**
* Copyright (C) 2011 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;
import android.os.Bundle;
import android.widget.TextView;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.crypto.SerializableKey;
import org.whispersystems.textsecure.storage.SessionRecord;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.whispersystems.textsecure.util.Hex;
import org.thoughtcrime.securesms.util.MemoryCleaner;
/**
* Activity for verifying session keys.
*
* @author Moxie Marlinspike
*
*/
public class VerifyKeysActivity extends KeyScanningActivity {
private byte[] yourFingerprintBytes;
private byte[] theirFingerprintBytes;
private TextView yourFingerprint;
private TextView theirFingerprint;
private Recipient recipient;
private MasterSecret masterSecret;
@Override
protected void onCreate(Bundle state) {
super.onCreate(state);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
setContentView(R.layout.verify_keys_activity);
initializeResources();
initializeFingerprints();
}
@Override
protected void onDestroy() {
MemoryCleaner.clean(masterSecret);
super.onDestroy();
}
private void initializeResources() {
this.recipient = (Recipient)this.getIntent().getParcelableExtra("recipient");
this.masterSecret = (MasterSecret)this.getIntent().getParcelableExtra("master_secret");
this.yourFingerprint = (TextView)findViewById(R.id.you_read);
this.theirFingerprint = (TextView)findViewById(R.id.friend_reads);
}
private void initializeFingerprints() {
SessionRecord session = new SessionRecord(this, masterSecret, recipient);
this.yourFingerprintBytes = session.getLocalFingerprint();
this.theirFingerprintBytes = session.getRemoteFingerprint();
this.yourFingerprint.setText(Hex.toString(yourFingerprintBytes));
this.theirFingerprint.setText(Hex.toString(theirFingerprintBytes));
}
@Override
protected String getDisplayString() {
return getString(R.string.VerifyKeysActivity_get_my_fingerprint_scanned);
}
@Override
protected String getScanString() {
return getString(R.string.VerifyKeysActivity_scan_their_fingerprint);
}
@Override
protected SerializableKey getIdentityKeyToCompare() {
return new FingerprintKey(theirFingerprintBytes);
}
@Override
protected SerializableKey getIdentityKeyToDisplay() {
return new FingerprintKey(yourFingerprintBytes);
}
@Override
protected String getNotVerifiedMessage() {
return getString(R.string.VerifyKeysActivity_warning_the_scanned_key_does_not_match_please_check_the_fingerprint_text_carefully2);
}
@Override
protected String getNotVerifiedTitle() {
return getString(R.string.VerifyKeysActivity_not_verified_exclamation);
}
@Override
protected String getVerifiedMessage() {
return getString(R.string.VerifyKeysActivity_their_key_is_correct_it_is_also_necessary_to_get_your_fingerprint_scanned_as_well);
}
@Override
protected String getVerifiedTitle() {
return getString(R.string.VerifyKeysActivity_verified_exclamation);
}
private class FingerprintKey implements SerializableKey {
private final byte[] fingerprint;
public FingerprintKey(byte[] fingerprint) {
this.fingerprint = fingerprint;
}
public byte[] serialize() {
return fingerprint;
}
}
}

View File

@ -40,18 +40,15 @@ import org.thoughtcrime.securesms.service.PushReceiver;
import org.thoughtcrime.securesms.service.SendReceiveService; import org.thoughtcrime.securesms.service.SendReceiveService;
import org.thoughtcrime.securesms.sms.SmsTransportDetails; import org.thoughtcrime.securesms.sms.SmsTransportDetails;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.textsecure.crypto.IdentityKeyPair;
import org.whispersystems.textsecure.crypto.InvalidKeyException; import org.whispersystems.textsecure.crypto.InvalidKeyException;
import org.whispersystems.textsecure.crypto.InvalidMessageException; import org.whispersystems.textsecure.crypto.InvalidMessageException;
import org.whispersystems.textsecure.crypto.InvalidVersionException; import org.whispersystems.textsecure.crypto.InvalidVersionException;
import org.whispersystems.textsecure.crypto.KeyUtil;
import org.whispersystems.textsecure.crypto.MasterSecret; import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.crypto.MessageCipher;
import org.whispersystems.textsecure.crypto.SessionCipher; import org.whispersystems.textsecure.crypto.SessionCipher;
import org.whispersystems.textsecure.crypto.ecc.Curve;
import org.whispersystems.textsecure.crypto.ecc.ECPublicKey;
import org.whispersystems.textsecure.push.IncomingPushMessage; import org.whispersystems.textsecure.push.IncomingPushMessage;
import org.whispersystems.textsecure.storage.Session;
import org.whispersystems.textsecure.util.Hex; import org.whispersystems.textsecure.util.Hex;
import org.whispersystems.textsecure.util.Util;
import java.io.IOException; import java.io.IOException;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
@ -188,29 +185,26 @@ public class DecryptingQueue {
} }
public void run() { public void run() {
synchronized (SessionCipher.CIPHER_LOCK) { try {
try { Recipients recipients = RecipientFactory.getRecipientsFromString(context, message.getSource(), false);
Recipients recipients = RecipientFactory.getRecipientsFromString(context, message.getSource(), false); Recipient recipient = recipients.getPrimaryRecipient();
Recipient recipient = recipients.getPrimaryRecipient();
if (!KeyUtil.isSessionFor(context, recipient)) { if (!Session.hasSession(context, masterSecret, recipient)) {
sendResult(PushReceiver.RESULT_NO_SESSION); sendResult(PushReceiver.RESULT_NO_SESSION);
return; return;
}
IdentityKeyPair identityKey = IdentityKeyUtil.getIdentityKeyPair(context, masterSecret, Curve.DJB_TYPE);
MessageCipher messageCipher = new MessageCipher(context, masterSecret, identityKey);
byte[] plaintextBody = messageCipher.decrypt(recipient, message.getBody());
message = message.withBody(plaintextBody);
sendResult(PushReceiver.RESULT_OK);
} catch (InvalidMessageException e) {
Log.w("DecryptionQueue", e);
sendResult(PushReceiver.RESULT_DECRYPT_FAILED);
} catch (RecipientFormattingException e) {
Log.w("DecryptionQueue", e);
sendResult(PushReceiver.RESULT_DECRYPT_FAILED);
} }
SessionCipher sessionCipher = SessionCipher.createFor(context, masterSecret, recipient);
byte[] plaintextBody = sessionCipher.decrypt(message.getBody());
message = message.withBody(plaintextBody);
sendResult(PushReceiver.RESULT_OK);
} catch (InvalidMessageException e) {
Log.w("DecryptionQueue", e);
sendResult(PushReceiver.RESULT_DECRYPT_FAILED);
} catch (RecipientFormattingException e) {
Log.w("DecryptionQueue", e);
sendResult(PushReceiver.RESULT_DECRYPT_FAILED);
} }
} }
@ -268,7 +262,7 @@ public class DecryptingQueue {
return; return;
} }
if (!KeyUtil.isSessionFor(context, recipient)) { if (!Session.hasSession(context, masterSecret, recipient)) {
Log.w("DecryptingQueue", "No such recipient session for MMS..."); Log.w("DecryptingQueue", "No such recipient session for MMS...");
database.markAsNoSession(messageId, threadId); database.markAsNoSession(messageId, threadId);
return; return;
@ -276,28 +270,24 @@ public class DecryptingQueue {
byte[] plaintextPduBytes; byte[] plaintextPduBytes;
synchronized (SessionCipher.CIPHER_LOCK) { Log.w("DecryptingQueue", "Decrypting: " + Hex.toString(ciphertextPduBytes));
Log.w("DecryptingQueue", "Decrypting: " + Hex.toString(ciphertextPduBytes)); TextTransport transportDetails = new TextTransport();
TextTransport transportDetails = new TextTransport(); SessionCipher sessionCipher = SessionCipher.createFor(context, masterSecret, recipient);
IdentityKeyPair identityKey = IdentityKeyUtil.getIdentityKeyPair(context, masterSecret, Curve.DJB_TYPE); byte[] decodedCiphertext = transportDetails.getDecodedMessage(ciphertextPduBytes);
MessageCipher messageCipher = new MessageCipher(context, masterSecret, identityKey);
byte[] ciphertext = transportDetails.getDecodedMessage(ciphertextPduBytes);
try { try {
plaintextPduBytes = messageCipher.decrypt(recipient, ciphertext); plaintextPduBytes = sessionCipher.decrypt(decodedCiphertext);
} catch (InvalidMessageException ime) { } catch (InvalidMessageException ime) {
// XXX - For some reason, Sprint seems to append a single character to the // XXX - 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 // end of message text segments. I don't know why, so here we just try
// truncating the message by one if the MAC fails. // truncating the message by one if the MAC fails.
if (ciphertextPduBytes.length > 2) { if (ciphertextPduBytes.length > 2) {
Log.w("DecryptingQueue", "Attempting truncated decrypt..."); Log.w("DecryptingQueue", "Attempting truncated decrypt...");
byte[] truncated = new byte[ciphertextPduBytes.length - 1]; byte[] truncated = Util.trim(ciphertextPduBytes, ciphertextPduBytes.length - 1);
System.arraycopy(ciphertextPduBytes, 0, truncated, 0, truncated.length); decodedCiphertext = transportDetails.getDecodedMessage(truncated);
ciphertext = transportDetails.getDecodedMessage(truncated); plaintextPduBytes = sessionCipher.decrypt(decodedCiphertext);
plaintextPduBytes = messageCipher.decrypt(recipient, ciphertext); } else {
} else { throw ime;
throw ime;
}
} }
} }
@ -352,36 +342,33 @@ public class DecryptingQueue {
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context); EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
String plaintextBody; String plaintextBody;
synchronized (SessionCipher.CIPHER_LOCK) { try {
try { Recipients recipients = RecipientFactory.getRecipientsFromString(context, originator, false);
Recipients recipients = RecipientFactory.getRecipientsFromString(context, originator, false); Recipient recipient = recipients.getPrimaryRecipient();
Recipient recipient = recipients.getPrimaryRecipient();
if (!KeyUtil.isSessionFor(context, recipient)) { if (!Session.hasSession(context, masterSecret, recipient)) {
database.markAsNoSession(messageId); database.markAsNoSession(messageId);
return;
}
SmsTransportDetails transportDetails = new SmsTransportDetails();
IdentityKeyPair identityKey = IdentityKeyUtil.getIdentityKeyPair(context, masterSecret, Curve.DJB_TYPE);
MessageCipher messageCipher = new MessageCipher(context, masterSecret, identityKey);
byte[] ciphertext = transportDetails.getDecodedMessage(body.getBytes());
byte[] paddedPlaintext = messageCipher.decrypt(recipient, ciphertext);
plaintextBody = new String(transportDetails.getStrippedPaddingMessageBody(paddedPlaintext));
} catch (InvalidMessageException e) {
Log.w("DecryptionQueue", e);
database.markAsDecryptFailed(messageId);
return;
} catch (RecipientFormattingException e) {
Log.w("DecryptionQueue", e);
database.markAsDecryptFailed(messageId);
return;
} catch (IOException e) {
Log.w("DecryptionQueue", e);
database.markAsDecryptFailed(messageId);
return; return;
} }
SmsTransportDetails transportDetails = new SmsTransportDetails();
SessionCipher sessionCipher = SessionCipher.createFor(context, masterSecret, recipient);
byte[] decodedCiphertext = transportDetails.getDecodedMessage(body.getBytes());
byte[] paddedPlaintext = sessionCipher.decrypt(decodedCiphertext);
plaintextBody = new String(transportDetails.getStrippedPaddingMessageBody(paddedPlaintext));
} catch (InvalidMessageException e) {
Log.w("DecryptionQueue", e);
database.markAsDecryptFailed(messageId);
return;
} catch (RecipientFormattingException e) {
Log.w("DecryptionQueue", e);
database.markAsDecryptFailed(messageId);
return;
} catch (IOException e) {
Log.w("DecryptionQueue", e);
database.markAsDecryptFailed(messageId);
return;
} }
database.updateMessageBody(masterSecret, messageId, plaintextBody); database.updateMessageBody(masterSecret, messageId, plaintextBody);
@ -414,22 +401,25 @@ public class DecryptingQueue {
private void handleKeyExchangeProcessing(String plaintxtBody) { private void handleKeyExchangeProcessing(String plaintxtBody) {
if (TextSecurePreferences.isAutoRespondKeyExchangeEnabled(context)) { if (TextSecurePreferences.isAutoRespondKeyExchangeEnabled(context)) {
try { try {
Recipient recipient = new Recipient(null, originator, null, null); Recipient recipient = new Recipient(null, originator, null, null);
KeyExchangeMessage keyExchangeMessage = new KeyExchangeMessage(plaintxtBody); KeyExchangeMessage message = KeyExchangeMessage.createFor(plaintxtBody);
KeyExchangeProcessor processor = new KeyExchangeProcessor(context, masterSecret, recipient); KeyExchangeProcessor processor = KeyExchangeProcessor.createFor(context, masterSecret, recipient, message);
Log.w("DecryptingQuue", "KeyExchange with fingerprint: " + keyExchangeMessage.getPublicKey().getFingerprint()); if (processor.isStale(message)) {
if (processor.isStale(keyExchangeMessage)) {
DatabaseFactory.getEncryptingSmsDatabase(context).markAsStaleKeyExchange(messageId); DatabaseFactory.getEncryptingSmsDatabase(context).markAsStaleKeyExchange(messageId);
} else if (processor.isTrusted(keyExchangeMessage)) { } else if (processor.isTrusted(message)) {
DatabaseFactory.getEncryptingSmsDatabase(context).markAsProcessedKeyExchange(messageId); DatabaseFactory.getEncryptingSmsDatabase(context).markAsProcessedKeyExchange(messageId);
processor.processKeyExchangeMessage(keyExchangeMessage, threadId); processor.processKeyExchangeMessage(message, threadId);
} }
} catch (InvalidVersionException e) { } catch (InvalidVersionException e) {
Log.w("DecryptingQueue", e); Log.w("DecryptingQueue", e);
DatabaseFactory.getEncryptingSmsDatabase(context).markAsInvalidVersionKeyExchange(messageId);
} catch (InvalidKeyException e) { } catch (InvalidKeyException e) {
Log.w("DecryptingQueue", e); Log.w("DecryptingQueue", e);
DatabaseFactory.getEncryptingSmsDatabase(context).markAsCorruptKeyExchange(messageId);
} catch (InvalidMessageException e) {
Log.w("DecryptingQueue", e);
DatabaseFactory.getEncryptingSmsDatabase(context).markAsCorruptKeyExchange(messageId);
} }
} }
} }

View File

@ -175,7 +175,7 @@ public class IdentityKeyUtil {
byte[] messageBytes = new byte[1 + PublicKey.KEY_SIZE]; byte[] messageBytes = new byte[1 + PublicKey.KEY_SIZE];
System.arraycopy(keyExchangeBytes, 0, messageBytes, 0, messageBytes.length); System.arraycopy(keyExchangeBytes, 0, messageBytes, 0, messageBytes.length);
byte[] publicKeyBytes = new byte[IdentityKey.SIZE]; byte[] publicKeyBytes = new byte[IdentityKey.NIST_SIZE];
System.arraycopy(keyExchangeBytes, messageBytes.length, publicKeyBytes, 0, publicKeyBytes.length); System.arraycopy(keyExchangeBytes, messageBytes.length, publicKeyBytes, 0, publicKeyBytes.length);
int signatureLength = Conversions.byteArrayToShort(keyExchangeBytes, messageBytes.length + publicKeyBytes.length); int signatureLength = Conversions.byteArrayToShort(keyExchangeBytes, messageBytes.length + publicKeyBytes.length);

View File

@ -20,17 +20,21 @@ package org.thoughtcrime.securesms.crypto;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.util.Log;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.crypto.protocol.KeyExchangeMessage; import org.thoughtcrime.securesms.crypto.protocol.KeyExchangeMessageV2;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.sms.MessageSender; import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.sms.OutgoingKeyExchangeMessage; import org.thoughtcrime.securesms.sms.OutgoingKeyExchangeMessage;
import org.whispersystems.textsecure.crypto.KeyUtil; import org.whispersystems.textsecure.crypto.IdentityKeyPair;
import org.whispersystems.textsecure.crypto.MasterSecret; import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.crypto.ecc.Curve;
import org.whispersystems.textsecure.crypto.ecc.ECKeyPair;
import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage; import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage;
import org.whispersystems.textsecure.storage.LocalKeyRecord; import org.whispersystems.textsecure.storage.SessionRecordV2;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
public class KeyExchangeInitiator { public class KeyExchangeInitiator {
@ -54,18 +58,42 @@ public class KeyExchangeInitiator {
} }
private static void initiateKeyExchange(Context context, MasterSecret masterSecret, Recipient recipient) { private static void initiateKeyExchange(Context context, MasterSecret masterSecret, Recipient recipient) {
LocalKeyRecord record = KeyUtil.initializeRecordFor(context, masterSecret, recipient, CiphertextMessage.CURVE25519_INTRODUCED_VERSION); int sequence = getRandomSequence();
KeyExchangeMessage message = new KeyExchangeMessage(context, masterSecret, CiphertextMessage.CURVE25519_INTRODUCED_VERSION, record, 0); int flags = KeyExchangeMessageV2.INITIATE_FLAG;
ECKeyPair baseKey = Curve.generateKeyPairForSession(CiphertextMessage.CURRENT_VERSION);
ECKeyPair ephemeralKey = Curve.generateKeyPairForSession(CiphertextMessage.CURRENT_VERSION);
IdentityKeyPair identityKey = IdentityKeyUtil.getIdentityKeyPair(context, masterSecret, Curve.DJB_TYPE);
KeyExchangeMessageV2 message = new KeyExchangeMessageV2(sequence, flags,
baseKey.getPublicKey(),
ephemeralKey.getPublicKey(),
identityKey.getPublicKey());
OutgoingKeyExchangeMessage textMessage = new OutgoingKeyExchangeMessage(recipient, message.serialize()); OutgoingKeyExchangeMessage textMessage = new OutgoingKeyExchangeMessage(recipient, message.serialize());
Log.w("SendKeyActivity", "Sending public key: " + record.getCurrentKeyPair().getPublicKey().getFingerprint()); SessionRecordV2 sessionRecordV2 = new SessionRecordV2(context, masterSecret, recipient);
sessionRecordV2.setPendingKeyExchange(sequence, baseKey, ephemeralKey, identityKey);
sessionRecordV2.save();
MessageSender.send(context, masterSecret, textMessage, -1); MessageSender.send(context, masterSecret, textMessage, -1);
} }
private static boolean hasInitiatedSession(Context context, MasterSecret masterSecret, Recipient recipient) { private static boolean hasInitiatedSession(Context context, MasterSecret masterSecret,
Recipient recipient)
{
return return
LocalKeyRecord.hasRecord(context, recipient) && new SessionRecordV2(context, masterSecret, recipient)
new LocalKeyRecord(context, masterSecret, recipient).getCurrentKeyPair() != null; .hasPendingKeyExchange();
}
private static int getRandomSequence() {
try {
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
int candidate = Math.abs(random.nextInt());
return candidate % 65535;
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
} }
} }

View File

@ -18,200 +18,25 @@
package org.thoughtcrime.securesms.crypto; package org.thoughtcrime.securesms.crypto;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.util.Log;
import org.thoughtcrime.securesms.crypto.protocol.KeyExchangeMessage; import org.thoughtcrime.securesms.crypto.protocol.KeyExchangeMessage;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.service.KeyCachingService; import org.whispersystems.textsecure.crypto.InvalidMessageException;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.sms.OutgoingKeyExchangeMessage;
import org.whispersystems.textsecure.crypto.IdentityKey;
import org.whispersystems.textsecure.crypto.InvalidKeyException;
import org.whispersystems.textsecure.crypto.KeyPair;
import org.whispersystems.textsecure.crypto.KeyUtil;
import org.whispersystems.textsecure.crypto.MasterSecret; import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.crypto.PublicKey;
import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage;
import org.whispersystems.textsecure.crypto.protocol.PreKeyBundleMessage;
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.whispersystems.textsecure.util.Conversions;
import org.whispersystems.textsecure.util.Medium;
/** public abstract class KeyExchangeProcessor {
* This class processes key exchange interactions.
*
* @author Moxie Marlinspike
*/
public class KeyExchangeProcessor {
public static final String SECURITY_UPDATE_EVENT = "org.thoughtcrime.securesms.KEY_EXCHANGE_UPDATE"; public static final String SECURITY_UPDATE_EVENT = "org.thoughtcrime.securesms.KEY_EXCHANGE_UPDATE";
private Context context; public abstract boolean isStale(KeyExchangeMessage message);
private Recipient recipient; public abstract boolean isTrusted(KeyExchangeMessage message);
private MasterSecret masterSecret; public abstract void processKeyExchangeMessage(KeyExchangeMessage message, long threadid)
private LocalKeyRecord localKeyRecord; throws InvalidMessageException;
private RemoteKeyRecord remoteKeyRecord;
private SessionRecord sessionRecord;
public KeyExchangeProcessor(Context context, MasterSecret masterSecret, Recipient recipient) { public static KeyExchangeProcessor createFor(Context context, MasterSecret masterSecret,
this.context = context; Recipient recipient, KeyExchangeMessage message)
this.recipient = recipient;
this.masterSecret = masterSecret;
this.remoteKeyRecord = new RemoteKeyRecord(context, recipient);
this.localKeyRecord = new LocalKeyRecord(context, masterSecret, recipient);
this.sessionRecord = new SessionRecord(context, masterSecret, recipient);
}
public boolean isTrusted(KeyExchangeMessage message) {
return message.hasIdentityKey() && 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);
}
public boolean hasInitiatedSession() {
return localKeyRecord.getCurrentKeyPair() != null;
}
private boolean needsResponseFromUs() {
return !hasInitiatedSession() || remoteKeyRecord.getCurrentRemoteKey() != null;
}
public boolean isStale(KeyExchangeMessage message) {
int responseKeyId = Conversions.highBitsToMedium(message.getPublicKey().getId());
Log.w("KeyExchangeProcessor", "Key Exchange High ID Bits: " + responseKeyId);
return responseKeyId != 0 &&
(localKeyRecord.getCurrentKeyPair() != null && localKeyRecord.getCurrentKeyPair().getId() != responseKeyId);
}
public void processKeyExchangeMessage(PreKeyBundleMessage message)
throws InvalidKeyIdException, InvalidKeyException
{ {
int preKeyId = message.getPreKeyId(); if (message.isLegacy()) return new KeyExchangeProcessorV1(context, masterSecret, recipient);
PublicKey remoteKey = message.getPublicKey(); else return new KeyExchangeProcessorV2(context, masterSecret, recipient);
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) && KeyUtil.isSessionFor(context, recipient)) {
Log.w("KeyExchangeProcessor", "We've already processed the prekey part, letting bundled message fall through...");
return;
}
if (!PreKeyRecord.hasRecord(context, preKeyId))
throw new InvalidKeyIdException("No such prekey: " + preKeyId);
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(), PreKeyBundleMessage.SUPPORTED_VERSION));
sessionRecord.setNegotiatedSessionVersion(sessionRecord.getSessionVersion());
localKeyRecord.save();
remoteKeyRecord.save();
sessionRecord.save();
if (preKeyId != Medium.MAX_VALUE) {
PreKeyRecord.delete(context, preKeyId);
}
DatabaseFactory.getIdentityDatabase(context)
.saveIdentity(masterSecret, recipient, remoteIdentity);
} }
}
public void processKeyExchangeMessage(PreKeyEntity message, long threadId) {
PublicKey remoteKey = new PublicKey(message.getKeyId(), message.getPublicKey());
remoteKeyRecord.setCurrentRemoteKey(remoteKey);
remoteKeyRecord.setLastRemoteKey(remoteKey);
remoteKeyRecord.save();
localKeyRecord = KeyUtil.initializeRecordFor(context, masterSecret, recipient, CiphertextMessage.SUPPORTED_VERSION);
localKeyRecord.setNextKeyPair(localKeyRecord.getCurrentKeyPair());
localKeyRecord.save();
sessionRecord.setSessionId(localKeyRecord.getCurrentKeyPair().getPublicKey().getFingerprintBytes(),
remoteKeyRecord.getCurrentRemoteKey().getFingerprintBytes());
sessionRecord.setIdentityKey(message.getIdentityKey());
sessionRecord.setNegotiatedSessionVersion(CiphertextMessage.SUPPORTED_VERSION);
sessionRecord.setSessionVersion(CiphertextMessage.SUPPORTED_VERSION);
sessionRecord.setPrekeyBundleRequired(true);
sessionRecord.save();
DatabaseFactory.getIdentityDatabase(context)
.saveIdentity(masterSecret, recipient, message.getIdentityKey());
broadcastSecurityUpdateEvent(context, threadId);
}
public void processKeyExchangeMessage(KeyExchangeMessage message, long threadId) {
int initiateKeyId = Conversions.lowBitsToMedium(message.getPublicKey().getId());
message.getPublicKey().setId(initiateKeyId);
if (needsResponseFromUs()) {
localKeyRecord = KeyUtil.initializeRecordFor(context, masterSecret, recipient, message.getMessageVersion());
KeyExchangeMessage ourMessage = new KeyExchangeMessage(context, masterSecret, Math.min(CiphertextMessage.SUPPORTED_VERSION, message.getMaxVersion()), localKeyRecord, initiateKeyId);
OutgoingKeyExchangeMessage textMessage = new OutgoingKeyExchangeMessage(recipient, ourMessage.serialize());
Log.w("KeyExchangeProcessor", "Responding with key exchange message fingerprint: " + ourMessage.getPublicKey().getFingerprint());
Log.w("KeyExchangeProcessor", "Which has a local key record fingerprint: " + localKeyRecord.getCurrentKeyPair().getPublicKey().getFingerprint());
MessageSender.send(context, masterSecret, textMessage, threadId);
}
remoteKeyRecord.setCurrentRemoteKey(message.getPublicKey());
remoteKeyRecord.setLastRemoteKey(message.getPublicKey());
remoteKeyRecord.save();
sessionRecord.setSessionId(localKeyRecord.getCurrentKeyPair().getPublicKey().getFingerprintBytes(),
remoteKeyRecord.getCurrentRemoteKey().getFingerprintBytes());
sessionRecord.setIdentityKey(message.getIdentityKey());
sessionRecord.setSessionVersion(Math.min(CiphertextMessage.SUPPORTED_VERSION, message.getMaxVersion()));
sessionRecord.setNegotiatedSessionVersion(sessionRecord.getSessionVersion());
Log.w("KeyExchangeUtil", "Setting session version: " + Math.min(CiphertextMessage.SUPPORTED_VERSION, message.getMaxVersion()));
sessionRecord.save();
if (message.hasIdentityKey()) {
DatabaseFactory.getIdentityDatabase(context)
.saveIdentity(masterSecret, recipient, message.getIdentityKey());
}
DecryptingQueue.scheduleRogueMessages(context, masterSecret, recipient);
broadcastSecurityUpdateEvent(context, threadId);
}
private static void broadcastSecurityUpdateEvent(Context context, long threadId) {
Intent intent = new Intent(SECURITY_UPDATE_EVENT);
intent.putExtra("thread_id", threadId);
intent.setPackage(context.getPackageName());
context.sendBroadcast(intent, KeyCachingService.KEY_PERMISSION);
}
}

View File

@ -0,0 +1,154 @@
package org.thoughtcrime.securesms.crypto;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import org.thoughtcrime.securesms.crypto.protocol.KeyExchangeMessage;
import org.thoughtcrime.securesms.crypto.protocol.KeyExchangeMessageV1;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.sms.OutgoingKeyExchangeMessage;
import org.whispersystems.textsecure.crypto.IdentityKey;
import org.whispersystems.textsecure.crypto.KeyPair;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.crypto.ecc.Curve;
import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage;
import org.whispersystems.textsecure.storage.LocalKeyRecord;
import org.whispersystems.textsecure.storage.RemoteKeyRecord;
import org.whispersystems.textsecure.storage.SessionRecordV1;
import org.whispersystems.textsecure.util.Conversions;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
/**
* This class processes key exchange interactions.
*
* @author Moxie Marlinspike
*/
public class KeyExchangeProcessorV1 extends KeyExchangeProcessor {
private Context context;
private Recipient recipient;
private MasterSecret masterSecret;
private LocalKeyRecord localKeyRecord;
private RemoteKeyRecord remoteKeyRecord;
private SessionRecordV1 sessionRecord;
public KeyExchangeProcessorV1(Context context, MasterSecret masterSecret, Recipient recipient) {
this.context = context;
this.recipient = recipient;
this.masterSecret = masterSecret;
this.remoteKeyRecord = new RemoteKeyRecord(context, recipient);
this.localKeyRecord = new LocalKeyRecord(context, masterSecret, recipient);
this.sessionRecord = new SessionRecordV1(context, masterSecret, recipient);
}
@Override
public boolean isTrusted(KeyExchangeMessage message) {
return message.hasIdentityKey() && isTrusted(message.getIdentityKey());
}
public boolean isTrusted(IdentityKey identityKey) {
return DatabaseFactory.getIdentityDatabase(context).isValidIdentity(masterSecret, recipient,
identityKey);
}
public boolean hasInitiatedSession() {
return localKeyRecord.getCurrentKeyPair() != null;
}
private boolean needsResponseFromUs() {
return !hasInitiatedSession() || remoteKeyRecord.getCurrentRemoteKey() != null;
}
@Override
public boolean isStale(KeyExchangeMessage _message) {
KeyExchangeMessageV1 message = (KeyExchangeMessageV1)_message;
int responseKeyId = Conversions.highBitsToMedium(message.getRemoteKey().getId());
Log.w("KeyExchangeProcessor", "Key Exchange High ID Bits: " + responseKeyId);
return responseKeyId != 0 &&
(localKeyRecord.getCurrentKeyPair() != null && localKeyRecord.getCurrentKeyPair().getId() != responseKeyId);
}
@Override
public void processKeyExchangeMessage(KeyExchangeMessage _message, long threadId) {
KeyExchangeMessageV1 message = (KeyExchangeMessageV1)_message;
int initiateKeyId = Conversions.lowBitsToMedium(message.getRemoteKey().getId());
message.getRemoteKey().setId(initiateKeyId);
if (needsResponseFromUs()) {
localKeyRecord = initializeRecordFor(context, masterSecret, recipient);
KeyExchangeMessageV1 ourMessage = new KeyExchangeMessageV1(context, masterSecret,
Math.min(CiphertextMessage.LEGACY_VERSION,
message.getMaxVersion()),
localKeyRecord, initiateKeyId);
OutgoingKeyExchangeMessage textMessage = new OutgoingKeyExchangeMessage(recipient, ourMessage.serialize());
Log.w("KeyExchangeProcessorV1", "Responding with key exchange message fingerprint: " + ourMessage.getRemoteKey().getFingerprint());
Log.w("KeyExchangeProcessorV1", "Which has a local key record fingerprint: " + localKeyRecord.getCurrentKeyPair().getPublicKey().getFingerprint());
MessageSender.send(context, masterSecret, textMessage, threadId);
}
remoteKeyRecord.setCurrentRemoteKey(message.getRemoteKey());
remoteKeyRecord.setLastRemoteKey(message.getRemoteKey());
remoteKeyRecord.save();
sessionRecord.setSessionId(localKeyRecord.getCurrentKeyPair().getPublicKey().getFingerprintBytes(),
remoteKeyRecord.getCurrentRemoteKey().getFingerprintBytes());
sessionRecord.setIdentityKey(message.getIdentityKey());
sessionRecord.setSessionVersion(Math.min(1, message.getMaxVersion()));
Log.w("KeyExchangeUtil", "Setting session version: " + Math.min(1, message.getMaxVersion()));
sessionRecord.save();
if (message.hasIdentityKey()) {
DatabaseFactory.getIdentityDatabase(context)
.saveIdentity(masterSecret, recipient, message.getIdentityKey());
}
DecryptingQueue.scheduleRogueMessages(context, masterSecret, recipient);
broadcastSecurityUpdateEvent(context, threadId);
}
private static void broadcastSecurityUpdateEvent(Context context, long threadId) {
Intent intent = new Intent(SECURITY_UPDATE_EVENT);
intent.putExtra("thread_id", threadId);
intent.setPackage(context.getPackageName());
context.sendBroadcast(intent, KeyCachingService.KEY_PERMISSION);
}
public LocalKeyRecord initializeRecordFor(Context context,
MasterSecret masterSecret,
Recipient recipient)
{
Log.w("KeyExchangeProcessorV1", "Initializing local key pairs...");
try {
SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
int initialId = secureRandom.nextInt(4094) + 1;
KeyPair currentPair = new KeyPair(initialId, Curve.generateKeyPairForSession(1), masterSecret);
KeyPair nextPair = new KeyPair(initialId + 1, Curve.generateKeyPairForSession(1), masterSecret);
LocalKeyRecord record = new LocalKeyRecord(context, masterSecret, recipient);
record.setCurrentKeyPair(currentPair);
record.setNextKeyPair(nextPair);
record.save();
return record;
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
}

View File

@ -0,0 +1,221 @@
package org.thoughtcrime.securesms.crypto;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import org.thoughtcrime.securesms.crypto.protocol.KeyExchangeMessage;
import org.thoughtcrime.securesms.crypto.protocol.KeyExchangeMessageV2;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.sms.OutgoingKeyExchangeMessage;
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.MasterSecret;
import org.whispersystems.textsecure.crypto.ecc.Curve;
import org.whispersystems.textsecure.crypto.ecc.ECKeyPair;
import org.whispersystems.textsecure.crypto.ecc.ECPublicKey;
import org.whispersystems.textsecure.crypto.protocol.PreKeyWhisperMessage;
import org.whispersystems.textsecure.crypto.ratchet.RatchetingSession;
import org.whispersystems.textsecure.push.PreKeyEntity;
import org.whispersystems.textsecure.storage.InvalidKeyIdException;
import org.whispersystems.textsecure.storage.PreKeyRecord;
import org.whispersystems.textsecure.storage.Session;
import org.whispersystems.textsecure.storage.SessionRecordV2;
import org.whispersystems.textsecure.util.Medium;
/**
* This class processes key exchange interactions.
*
* @author Moxie Marlinspike
*/
public class KeyExchangeProcessorV2 extends KeyExchangeProcessor {
private Context context;
private Recipient recipient;
private MasterSecret masterSecret;
private SessionRecordV2 sessionRecord;
public KeyExchangeProcessorV2(Context context, MasterSecret masterSecret, Recipient recipient) {
this.context = context;
this.recipient = recipient;
this.masterSecret = masterSecret;
this.sessionRecord = new SessionRecordV2(context, masterSecret, recipient);
}
public boolean isTrusted(PreKeyWhisperMessage message) {
return isTrusted(message.getIdentityKey());
}
public boolean isTrusted(KeyExchangeMessage message) {
return message.hasIdentityKey() && isTrusted(message.getIdentityKey());
}
public boolean isTrusted(IdentityKey identityKey) {
return DatabaseFactory.getIdentityDatabase(context).isValidIdentity(masterSecret, recipient,
identityKey);
}
public boolean isStale(KeyExchangeMessage m) {
KeyExchangeMessageV2 message = (KeyExchangeMessageV2)m;
return
message.isResponse() &&
(!sessionRecord.hasPendingKeyExchange() ||
sessionRecord.getPendingKeyExchangeSequence() != message.getSequence()) &&
!message.isResponseForSimultaneousInitiate();
}
public void processKeyExchangeMessage(PreKeyWhisperMessage message)
throws InvalidKeyIdException, InvalidKeyException
{
int preKeyId = message.getPreKeyId();
ECPublicKey theirBaseKey = message.getBaseKey();
ECPublicKey theirEphemeralKey = message.getWhisperMessage().getSenderEphemeral();
IdentityKey theirIdentityKey = message.getIdentityKey();
Log.w("KeyExchangeProcessor", "Received pre-key with local key ID: " + preKeyId);
if (!PreKeyRecord.hasRecord(context, preKeyId) && Session.hasSession(context, masterSecret, recipient)) {
Log.w("KeyExchangeProcessor", "We've already processed the prekey part, letting bundled message fall through...");
return;
}
if (!PreKeyRecord.hasRecord(context, preKeyId))
throw new InvalidKeyIdException("No such prekey: " + preKeyId);
PreKeyRecord preKeyRecord = new PreKeyRecord(context, masterSecret, preKeyId);
ECKeyPair ourBaseKey = preKeyRecord.getKeyPair().getKeyPair();
ECKeyPair ourEphemeralKey = ourBaseKey;
IdentityKeyPair ourIdentityKey = IdentityKeyUtil.getIdentityKeyPair(context, masterSecret, ourBaseKey.getPublicKey().getType());
sessionRecord.clear();
RatchetingSession.initializeSession(sessionRecord, ourBaseKey, theirBaseKey, ourEphemeralKey,
theirEphemeralKey, ourIdentityKey, theirIdentityKey);
sessionRecord.save();
if (preKeyId != Medium.MAX_VALUE) {
PreKeyRecord.delete(context, preKeyId);
}
DatabaseFactory.getIdentityDatabase(context)
.saveIdentity(masterSecret, recipient, theirIdentityKey);
}
public void processKeyExchangeMessage(PreKeyEntity message, long threadId)
throws InvalidKeyException
{
ECKeyPair ourBaseKey = Curve.generateKeyPairForSession(2);
ECKeyPair ourEphemeralKey = Curve.generateKeyPairForSession(2);
ECPublicKey theirBaseKey = message.getPublicKey().getPublicKey();
ECPublicKey theirEphemeralKey = theirBaseKey;
IdentityKey theirIdentityKey = message.getIdentityKey();
IdentityKeyPair ourIdentityKey = IdentityKeyUtil.getIdentityKeyPair(context, masterSecret,
ourBaseKey.getPublicKey()
.getType());
sessionRecord.clear();
RatchetingSession.initializeSession(sessionRecord, ourBaseKey, theirBaseKey, ourEphemeralKey,
theirEphemeralKey, ourIdentityKey, theirIdentityKey);
sessionRecord.setPendingPreKey(message.getKeyId(), ourBaseKey.getPublicKey());
sessionRecord.save();
DatabaseFactory.getIdentityDatabase(context)
.saveIdentity(masterSecret, recipient, message.getIdentityKey());
broadcastSecurityUpdateEvent(context, threadId);
}
@Override
public void processKeyExchangeMessage(KeyExchangeMessage _message, long threadId)
throws InvalidMessageException
{
try {
KeyExchangeMessageV2 message = (KeyExchangeMessageV2)_message;
Log.w("KeyExchangeProcessorV2", "Received key exchange with sequence: " + message.getSequence());
if (message.isInitiate()) {
ECKeyPair ourBaseKey, ourEphemeralKey;
IdentityKeyPair ourIdentityKey;
int flags = KeyExchangeMessageV2.RESPONSE_FLAG;
Log.w("KeyExchangeProcessorV2", "KeyExchange is an initiate.");
if (!sessionRecord.hasPendingKeyExchange()) {
Log.w("KeyExchangeProcessorV2", "We don't have a pending initiate...");
ourBaseKey = Curve.generateKeyPairForType(message.getBaseKey().getType());
ourEphemeralKey = Curve.generateKeyPairForType(message.getBaseKey().getType());
ourIdentityKey = IdentityKeyUtil.getIdentityKeyPair(context, masterSecret, message.getBaseKey().getType());
sessionRecord.setPendingKeyExchange(message.getSequence(), ourBaseKey, ourEphemeralKey,
ourIdentityKey);
} else {
Log.w("KeyExchangeProcessorV2", "We alredy have a pending initiate, responding as simultaneous initiate...");
ourBaseKey = sessionRecord.getPendingKeyExchangeBaseKey();
ourEphemeralKey = sessionRecord.getPendingKeyExchangeEphemeralKey();
ourIdentityKey = sessionRecord.getPendingKeyExchangeIdentityKey();
flags |= KeyExchangeMessageV2.SIMULTAENOUS_INITIATE_FLAG;
sessionRecord.setPendingKeyExchange(message.getSequence(), ourBaseKey, ourEphemeralKey,
ourIdentityKey);
}
KeyExchangeMessageV2 ourMessage = new KeyExchangeMessageV2(message.getSequence(),
flags, ourBaseKey.getPublicKey(),
ourEphemeralKey.getPublicKey(),
ourIdentityKey.getPublicKey());
OutgoingKeyExchangeMessage textMessage = new OutgoingKeyExchangeMessage(recipient,
ourMessage.serialize());
MessageSender.send(context, masterSecret, textMessage, threadId);
}
if (message.getSequence() != sessionRecord.getPendingKeyExchangeSequence()) {
Log.w("KeyExchangeProcessorV2", "No matching sequence for response. " +
"Is simultaneous initiate response: " + message.isResponseForSimultaneousInitiate());
return;
}
ECKeyPair ourBaseKey = sessionRecord.getPendingKeyExchangeBaseKey();
ECKeyPair ourEphemeralKey = sessionRecord.getPendingKeyExchangeEphemeralKey();
IdentityKeyPair ourIdentityKey = sessionRecord.getPendingKeyExchangeIdentityKey();
sessionRecord.clear();
RatchetingSession.initializeSession(sessionRecord, ourBaseKey, message.getBaseKey(),
ourEphemeralKey, message.getEphemeralKey(),
ourIdentityKey, message.getIdentityKey());
sessionRecord.setSessionVersion(message.getVersion());
Session.clearV1SessionFor(context, recipient);
sessionRecord.save();
DatabaseFactory.getIdentityDatabase(context)
.saveIdentity(masterSecret, recipient, message.getIdentityKey());
DecryptingQueue.scheduleRogueMessages(context, masterSecret, recipient);
broadcastSecurityUpdateEvent(context, threadId);
} catch (InvalidKeyException e) {
throw new InvalidMessageException(e);
}
}
private static void broadcastSecurityUpdateEvent(Context context, long threadId) {
Intent intent = new Intent(KeyExchangeProcessorV1.SECURITY_UPDATE_EVENT);
intent.putExtra("thread_id", threadId);
intent.setPackage(context.getPackageName());
context.sendBroadcast(intent, KeyCachingService.KEY_PERMISSION);
}
}

View File

@ -17,151 +17,39 @@
*/ */
package org.thoughtcrime.securesms.crypto.protocol; package org.thoughtcrime.securesms.crypto.protocol;
import android.content.Context;
import android.util.Log;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.whispersystems.textsecure.crypto.IdentityKey; import org.whispersystems.textsecure.crypto.IdentityKey;
import org.whispersystems.textsecure.crypto.InvalidKeyException; import org.whispersystems.textsecure.crypto.InvalidKeyException;
import org.whispersystems.textsecure.crypto.InvalidMessageException;
import org.whispersystems.textsecure.crypto.InvalidVersionException; import org.whispersystems.textsecure.crypto.InvalidVersionException;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.crypto.PublicKey; import org.whispersystems.textsecure.crypto.PublicKey;
import org.whispersystems.textsecure.crypto.ecc.Curve;
import org.whispersystems.textsecure.crypto.ecc.ECPublicKey; import org.whispersystems.textsecure.crypto.ecc.ECPublicKey;
import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage; import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage;
import org.whispersystems.textsecure.storage.LocalKeyRecord;
import org.whispersystems.textsecure.util.Base64; import org.whispersystems.textsecure.util.Base64;
import org.whispersystems.textsecure.util.Conversions; import org.whispersystems.textsecure.util.Conversions;
import org.whispersystems.textsecure.util.Util;
import java.io.IOException; import java.io.IOException;
/**
* A class for constructing and parsing key exchange messages.
*
* A key exchange message is basically represented by the following format:
*
* 1) 4 bits <protocol version number>
* 2) 4 bits <max supported protocol version number>
* 3) A serialized public key
* 4) (Optional) An identity key.
* 5) (if #4) A signature over the identity key, version bits, and serialized public key.
*
* A serialized public key is basically represented by the following format:
*
* 1) A 3 byte key ID.
* 2) An ECC key encoded with point compression.
*
* An initiating key ID is initialized with the bottom 12 bits of the ID set. A responding key
* ID does the same, but puts the initiating key ID's bottom 12 bits in the top 12 bits of its
* ID. This is used to correlate key exchange responses.
*
* @author Moxie Marlinspike
*
*/
public class KeyExchangeMessage { public abstract class KeyExchangeMessage {
public abstract boolean isLegacy();
public abstract IdentityKey getIdentityKey();
public abstract boolean hasIdentityKey();
public abstract int getMaxVersion();
public abstract int getVersion();
private final int messageVersion; public static KeyExchangeMessage createFor(String rawMessage)
private final int supportedVersion; throws InvalidMessageException, InvalidKeyException, InvalidVersionException
private final PublicKey publicKey; {
private final String serialized;
private IdentityKey identityKey;
public KeyExchangeMessage(Context context, MasterSecret masterSecret, int messageVersion, LocalKeyRecord record, int highIdBits) {
this.publicKey = new PublicKey(record.getCurrentKeyPair().getPublicKey());
this.messageVersion = messageVersion;
this.supportedVersion = CiphertextMessage.SUPPORTED_VERSION;
publicKey.setId(publicKey.getId() | (highIdBits << 12));
byte[] versionBytes = {Conversions.intsToByteHighAndLow(messageVersion, supportedVersion)};
byte[] publicKeyBytes = publicKey.serialize();
byte[] serializedBytes;
if (includeIdentityNoSignature(messageVersion, context)) {
byte[] identityKey = IdentityKeyUtil.getIdentityKey(context, Curve.DJB_TYPE).serialize();
serializedBytes = Util.combine(versionBytes, publicKeyBytes, identityKey);
} else if (includeIdentitySignature(messageVersion, context)) {
byte[] prolog = Util.combine(versionBytes, publicKeyBytes);
serializedBytes = IdentityKeyUtil.getSignedKeyExchange(context, masterSecret, prolog);
} else {
serializedBytes = Util.combine(versionBytes, publicKeyBytes);
}
if (messageVersion < 1) this.serialized = Base64.encodeBytes(serializedBytes);
else this.serialized = Base64.encodeBytesWithoutPadding(serializedBytes);
}
public KeyExchangeMessage(String messageBody) throws InvalidVersionException, InvalidKeyException {
try { try {
byte[] keyBytes = Base64.decode(messageBody); byte[] decodedMessage = Base64.decodeWithoutPadding(rawMessage);
this.messageVersion = Conversions.highBitsToInt(keyBytes[0]);
this.supportedVersion = Conversions.lowBitsToInt(keyBytes[0]);
this.serialized = messageBody;
if (messageVersion > CiphertextMessage.SUPPORTED_VERSION)
throw new InvalidVersionException("Key exchange with version: " + messageVersion);
if (messageVersion >= 1) if (Conversions.highBitsToInt(decodedMessage[0]) <= CiphertextMessage.LEGACY_VERSION) {
keyBytes = Base64.decodeWithoutPadding(messageBody); return new KeyExchangeMessageV1(rawMessage);
} else {
this.publicKey = new PublicKey(keyBytes, 1); return new KeyExchangeMessageV2(rawMessage);
if (keyBytes.length <= PublicKey.KEY_SIZE + 1) {
this.identityKey = null;
} else if (messageVersion == 1) {
try {
this.identityKey = IdentityKeyUtil.verifySignedKeyExchange(keyBytes);
} catch (InvalidKeyException ike) {
Log.w("KeyUtil", ike);
this.identityKey = null;
}
} else if (messageVersion == 2) {
try {
this.identityKey = new IdentityKey(keyBytes, 1 + PublicKey.KEY_SIZE);
} catch (InvalidKeyException ike) {
Log.w("KeyUtil", ike);
this.identityKey = null;
}
} }
} catch (IOException ioe) { } catch (IOException e) {
throw new InvalidKeyException(ioe); throw new InvalidMessageException(e);
} }
} }
}
private static boolean includeIdentitySignature(int messageVersion, Context context) {
return IdentityKeyUtil.hasIdentityKey(context, Curve.NIST_TYPE) && (messageVersion == 1);
}
private static boolean includeIdentityNoSignature(int messageVersion, Context context) {
return IdentityKeyUtil.hasIdentityKey(context, Curve.DJB_TYPE) && (messageVersion >= 2);
}
public PublicKey getPublicKey() {
return publicKey;
}
public IdentityKey getIdentityKey() {
return identityKey;
}
public int getMaxVersion() {
return supportedVersion;
}
public int getMessageVersion() {
return messageVersion;
}
public boolean hasIdentityKey() {
return identityKey != null;
}
public String serialize() {
return serialized;
}
}

View File

@ -0,0 +1,160 @@
package org.thoughtcrime.securesms.crypto.protocol;
import android.content.Context;
import android.util.Log;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.whispersystems.textsecure.crypto.IdentityKey;
import org.whispersystems.textsecure.crypto.InvalidKeyException;
import org.whispersystems.textsecure.crypto.InvalidVersionException;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.crypto.PublicKey;
import org.whispersystems.textsecure.crypto.ecc.Curve;
import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage;
import org.whispersystems.textsecure.storage.LocalKeyRecord;
import org.whispersystems.textsecure.util.Base64;
import org.whispersystems.textsecure.util.Conversions;
import org.whispersystems.textsecure.util.Util;
import java.io.IOException;
/**
* A class for constructing and parsing key exchange messages.
*
* A key exchange message is basically represented by the following format:
*
* 1) 4 bits <protocol version number>
* 2) 4 bits <max supported protocol version number>
* 3) A serialized public key
* 4) (Optional) An identity key.
* 5) (if #4) A signature over the identity key, version bits, and serialized public key.
*
* A serialized public key is basically represented by the following format:
*
* 1) A 3 byte key ID.
* 2) An ECC key encoded with point compression.
*
* An initiating key ID is initialized with the bottom 12 bits of the ID set. A responding key
* ID does the same, but puts the initiating key ID's bottom 12 bits in the top 12 bits of its
* ID. This is used to correlate key exchange responses.
*
* @author Moxie Marlinspike
*
*/
public class KeyExchangeMessageV1 extends KeyExchangeMessage {
private final int messageVersion;
private final int supportedVersion;
private final PublicKey publicKey;
private final String serialized;
private IdentityKey identityKey;
public KeyExchangeMessageV1(Context context, MasterSecret masterSecret,
int messageVersion, LocalKeyRecord record, int highIdBits)
{
this.publicKey = new PublicKey(record.getCurrentKeyPair().getPublicKey());
this.messageVersion = messageVersion;
this.supportedVersion = CiphertextMessage.CURRENT_VERSION;
publicKey.setId(publicKey.getId() | (highIdBits << 12));
byte[] versionBytes = {Conversions.intsToByteHighAndLow(messageVersion, supportedVersion)};
byte[] publicKeyBytes = publicKey.serialize();
byte[] serializedBytes;
if (includeIdentityNoSignature(messageVersion, context)) {
byte[] identityKey = IdentityKeyUtil.getIdentityKey(context, Curve.DJB_TYPE).serialize();
serializedBytes = Util.combine(versionBytes, publicKeyBytes, identityKey);
} else if (includeIdentitySignature(messageVersion, context)) {
byte[] prolog = Util.combine(versionBytes, publicKeyBytes);
serializedBytes = IdentityKeyUtil.getSignedKeyExchange(context, masterSecret, prolog);
} else {
serializedBytes = Util.combine(versionBytes, publicKeyBytes);
}
if (messageVersion < 1) this.serialized = Base64.encodeBytes(serializedBytes);
else this.serialized = Base64.encodeBytesWithoutPadding(serializedBytes);
}
public KeyExchangeMessageV1(String messageBody) throws InvalidVersionException, InvalidKeyException {
try {
byte[] keyBytes = Base64.decode(messageBody);
this.messageVersion = Conversions.highBitsToInt(keyBytes[0]);
this.supportedVersion = Conversions.lowBitsToInt(keyBytes[0]);
this.serialized = messageBody;
if (messageVersion > 1)
throw new InvalidVersionException("Legacy key exchange with version: " + messageVersion);
if (messageVersion >= 1)
keyBytes = Base64.decodeWithoutPadding(messageBody);
this.publicKey = new PublicKey(keyBytes, 1);
if (keyBytes.length <= PublicKey.KEY_SIZE + 1) {
this.identityKey = null;
} else if (messageVersion == 1) {
try {
this.identityKey = IdentityKeyUtil.verifySignedKeyExchange(keyBytes);
} catch (InvalidKeyException ike) {
Log.w("KeyUtil", ike);
this.identityKey = null;
}
} else if (messageVersion == 2) {
try {
this.identityKey = new IdentityKey(keyBytes, 1 + PublicKey.KEY_SIZE);
} catch (InvalidKeyException ike) {
Log.w("KeyUtil", ike);
this.identityKey = null;
}
}
} catch (IOException ioe) {
throw new InvalidKeyException(ioe);
}
}
private static boolean includeIdentitySignature(int messageVersion, Context context) {
return IdentityKeyUtil.hasIdentityKey(context, Curve.NIST_TYPE) && (messageVersion == 1);
}
private static boolean includeIdentityNoSignature(int messageVersion, Context context) {
return IdentityKeyUtil.hasIdentityKey(context, Curve.DJB_TYPE) && (messageVersion >= 2);
}
@Override
public boolean isLegacy() {
return true;
}
@Override
public IdentityKey getIdentityKey() {
return identityKey;
}
public PublicKey getRemoteKey() {
return publicKey;
}
@Override
public int getMaxVersion() {
return supportedVersion;
}
@Override
public int getVersion() {
return messageVersion;
}
@Override
public boolean hasIdentityKey() {
return identityKey != null;
}
public String serialize() {
return serialized;
}
}

View File

@ -0,0 +1,152 @@
package org.thoughtcrime.securesms.crypto.protocol;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import org.whispersystems.textsecure.crypto.IdentityKey;
import org.whispersystems.textsecure.crypto.InvalidKeyException;
import org.whispersystems.textsecure.crypto.InvalidMessageException;
import org.whispersystems.textsecure.crypto.InvalidVersionException;
import org.whispersystems.textsecure.crypto.ecc.Curve;
import org.whispersystems.textsecure.crypto.ecc.ECPublicKey;
import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage;
import org.whispersystems.textsecure.crypto.protocol.WhisperProtos;
import org.whispersystems.textsecure.util.Base64;
import org.whispersystems.textsecure.util.Conversions;
import org.whispersystems.textsecure.util.Util;
import java.io.IOException;
public class KeyExchangeMessageV2 extends KeyExchangeMessage {
public static final int INITIATE_FLAG = 0x01;
public static final int RESPONSE_FLAG = 0X02;
public static final int SIMULTAENOUS_INITIATE_FLAG = 0x04;
private final int version;
private final int supportedVersion;
private final int sequence;
private final int flags;
private final ECPublicKey baseKey;
private final ECPublicKey ephemeralKey;
private final IdentityKey identityKey;
private final byte[] serialized;
public KeyExchangeMessageV2(int sequence, int flags,
ECPublicKey baseKey, ECPublicKey ephemeralKey,
IdentityKey identityKey)
{
this.supportedVersion = CiphertextMessage.CURRENT_VERSION;
this.version = CiphertextMessage.CURRENT_VERSION;
this.sequence = sequence;
this.flags = flags;
this.baseKey = baseKey;
this.ephemeralKey = ephemeralKey;
this.identityKey = identityKey;
byte[] version = {Conversions.intsToByteHighAndLow(this.version, this.supportedVersion)};
byte[] message = WhisperProtos.KeyExchangeMessage.newBuilder()
.setId((sequence << 5) | flags)
.setBaseKey(ByteString.copyFrom(baseKey.serialize()))
.setEphemeralKey(ByteString.copyFrom(ephemeralKey.serialize()))
.setIdentityKey(ByteString.copyFrom(identityKey.serialize()))
.build().toByteArray();
this.serialized = Util.combine(version, message);
}
public KeyExchangeMessageV2(String serializedAndEncoded)
throws InvalidMessageException, InvalidVersionException
{
try {
byte[] serialized = Base64.decodeWithoutPadding(serializedAndEncoded);
byte[][] parts = Util.split(serialized, 1, serialized.length - 1);
this.version = Conversions.highBitsToInt(parts[0][0]);
this.supportedVersion = Conversions.lowBitsToInt(parts[0][0]);
if (this.version > CiphertextMessage.CURRENT_VERSION) {
throw new InvalidVersionException("Unknown version: " + this.version);
}
WhisperProtos.KeyExchangeMessage message = WhisperProtos.KeyExchangeMessage.parseFrom(parts[1]);
if (!message.hasId() || !message.hasBaseKey() ||
!message.hasEphemeralKey() || !message.hasIdentityKey())
{
throw new InvalidMessageException("Some required fields missing!");
}
this.sequence = message.getId() >> 5;
this.flags = message.getId() & 0x1f;
this.serialized = serialized;
this.baseKey = Curve.decodePoint(message.getBaseKey().toByteArray(), 0);
this.ephemeralKey = Curve.decodePoint(message.getEphemeralKey().toByteArray(), 0);
this.identityKey = new IdentityKey(message.getIdentityKey().toByteArray(), 0);
} catch (InvalidProtocolBufferException e) {
throw new InvalidMessageException(e);
} catch (InvalidKeyException e) {
throw new InvalidMessageException(e);
} catch (IOException e) {
throw new InvalidMessageException(e);
}
}
@Override
public int getVersion() {
return version;
}
public ECPublicKey getBaseKey() {
return baseKey;
}
public ECPublicKey getEphemeralKey() {
return ephemeralKey;
}
@Override
public boolean isLegacy() {
return false;
}
@Override
public IdentityKey getIdentityKey() {
return identityKey;
}
@Override
public boolean hasIdentityKey() {
return true;
}
@Override
public int getMaxVersion() {
return supportedVersion;
}
public boolean isResponse() {
return ((flags & RESPONSE_FLAG) != 0);
}
public boolean isInitiate() {
return (flags & INITIATE_FLAG) != 0;
}
public boolean isResponseForSimultaneousInitiate() {
return (flags & SIMULTAENOUS_INITIATE_FLAG) != 0;
}
public int getFlags() {
return flags;
}
public int getSequence() {
return sequence;
}
public String serialize() {
return Base64.encodeBytesWithoutPadding(serialized);
}
}

View File

@ -30,8 +30,8 @@ import org.whispersystems.textsecure.crypto.IdentityKey;
import org.whispersystems.textsecure.crypto.InvalidMessageException; import org.whispersystems.textsecure.crypto.InvalidMessageException;
import org.whispersystems.textsecure.crypto.MasterCipher; import org.whispersystems.textsecure.crypto.MasterCipher;
import org.whispersystems.textsecure.crypto.MasterSecret; import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.storage.SessionRecord;
import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.whispersystems.textsecure.storage.Session;
import org.whispersystems.textsecure.util.Base64; import org.whispersystems.textsecure.util.Base64;
import org.whispersystems.textsecure.util.Util; import org.whispersystems.textsecure.util.Util;
@ -392,8 +392,7 @@ public class DatabaseFactory {
if (name.matches("[0-9]+")) { if (name.matches("[0-9]+")) {
long recipientId = Long.parseLong(name); long recipientId = Long.parseLong(name);
SessionRecord sessionRecord = new SessionRecord(context, masterSecret, recipientId); IdentityKey identityKey = Session.getRemoteIdentityKey(context, masterSecret, recipientId);
IdentityKey identityKey = sessionRecord.getIdentityKey();
if (identityKey != null) { if (identityKey != null) {
MasterCipher masterCipher = new MasterCipher(masterSecret); MasterCipher masterCipher = new MasterCipher(masterSecret);

View File

@ -166,6 +166,10 @@ public class SmsDatabase extends Database implements MmsSmsColumns {
updateTypeBitmask(id, 0, Types.KEY_EXCHANGE_CORRUPTED_BIT); updateTypeBitmask(id, 0, Types.KEY_EXCHANGE_CORRUPTED_BIT);
} }
public void markAsInvalidVersionKeyExchange(long id) {
updateTypeBitmask(id, 0, Types.KEY_EXCHANGE_INVALID_VERSION_BIT);
}
public void markAsSecure(long id) { public void markAsSecure(long id) {
updateTypeBitmask(id, 0, Types.SECURE_MESSAGE_BIT); updateTypeBitmask(id, 0, Types.SECURE_MESSAGE_BIT);
} }

View File

@ -8,7 +8,7 @@ import android.util.Pair;
import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.InvalidProtocolBufferException;
import org.thoughtcrime.securesms.crypto.DecryptingQueue; import org.thoughtcrime.securesms.crypto.DecryptingQueue;
import org.thoughtcrime.securesms.crypto.KeyExchangeProcessor; import org.thoughtcrime.securesms.crypto.KeyExchangeProcessorV2;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.EncryptingSmsDatabase; import org.thoughtcrime.securesms.database.EncryptingSmsDatabase;
import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.MmsDatabase;
@ -24,9 +24,10 @@ import org.thoughtcrime.securesms.sms.IncomingTextMessage;
import org.thoughtcrime.securesms.sms.SmsTransportDetails; import org.thoughtcrime.securesms.sms.SmsTransportDetails;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.textsecure.crypto.InvalidKeyException; import org.whispersystems.textsecure.crypto.InvalidKeyException;
import org.whispersystems.textsecure.crypto.InvalidMessageException;
import org.whispersystems.textsecure.crypto.InvalidVersionException; import org.whispersystems.textsecure.crypto.InvalidVersionException;
import org.whispersystems.textsecure.crypto.MasterSecret; import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.crypto.protocol.PreKeyBundleMessage; import org.whispersystems.textsecure.crypto.protocol.PreKeyWhisperMessage;
import org.whispersystems.textsecure.push.IncomingPushMessage; import org.whispersystems.textsecure.push.IncomingPushMessage;
import org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent; import org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent;
import org.whispersystems.textsecure.storage.InvalidKeyIdException; import org.whispersystems.textsecure.storage.InvalidKeyIdException;
@ -46,9 +47,9 @@ public class PushReceiver {
} }
public void process(MasterSecret masterSecret, Intent intent) { public void process(MasterSecret masterSecret, Intent intent) {
if (intent.getAction().equals(SendReceiveService.RECEIVE_PUSH_ACTION)) { if (SendReceiveService.RECEIVE_PUSH_ACTION.equals(intent.getAction())) {
handleMessage(masterSecret, intent); handleMessage(masterSecret, intent);
} else if (intent.getAction().equals(SendReceiveService.DECRYPTED_PUSH_ACTION)) { } else if (SendReceiveService.DECRYPTED_PUSH_ACTION.equals(intent.getAction())) {
handleDecrypt(masterSecret, intent); handleDecrypt(masterSecret, intent);
} }
} }
@ -81,25 +82,25 @@ public class PushReceiver {
} else { } else {
Recipients recipients = RecipientFactory.getRecipientsFromMessage(context, message, false); Recipients recipients = RecipientFactory.getRecipientsFromMessage(context, message, false);
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipients); long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipients);
MessageNotifier.updateNotification(context, masterSecret, threadId); MessageNotifier.updateNotification(context, null, threadId);
} }
} }
private void handleReceivedPreKeyBundle(MasterSecret masterSecret, IncomingPushMessage message) { private void handleReceivedPreKeyBundle(MasterSecret masterSecret, IncomingPushMessage message) {
if (masterSecret == null) { if (masterSecret == null) {
handleReceivedSecureMessage(masterSecret, message); handleReceivedSecureMessage(null, message);
return; return;
} }
try { try {
Recipient recipient = new Recipient(null, message.getSource(), null, null); Recipient recipient = new Recipient(null, message.getSource(), null, null);
KeyExchangeProcessor processor = new KeyExchangeProcessor(context, masterSecret, recipient); KeyExchangeProcessorV2 processor = new KeyExchangeProcessorV2(context, masterSecret, recipient);
PreKeyBundleMessage preKeyExchange = new PreKeyBundleMessage(message.getBody()); PreKeyWhisperMessage preKeyExchange = new PreKeyWhisperMessage(message.getBody());
if (processor.isTrusted(preKeyExchange)) { if (processor.isTrusted(preKeyExchange)) {
processor.processKeyExchangeMessage(preKeyExchange); processor.processKeyExchangeMessage(preKeyExchange);
IncomingPushMessage bundledMessage = message.withBody(preKeyExchange.getBundledMessage().serialize()); IncomingPushMessage bundledMessage = message.withBody(preKeyExchange.getWhisperMessage().serialize());
handleReceivedSecureMessage(masterSecret, bundledMessage); handleReceivedSecureMessage(masterSecret, bundledMessage);
} else { } else {
SmsTransportDetails transportDetails = new SmsTransportDetails(); SmsTransportDetails transportDetails = new SmsTransportDetails();
@ -110,13 +111,16 @@ public class PushReceiver {
DatabaseFactory.getEncryptingSmsDatabase(context).insertMessageInbox(masterSecret, textMessage); DatabaseFactory.getEncryptingSmsDatabase(context).insertMessageInbox(masterSecret, textMessage);
} }
} catch (InvalidKeyException e) { } catch (InvalidKeyException e) {
Log.w("SmsReceiver", e); Log.w("PushReceiver", e);
handleReceivedCorruptedKey(masterSecret, message, false); handleReceivedCorruptedKey(masterSecret, message, false);
} catch (InvalidVersionException e) { } catch (InvalidVersionException e) {
Log.w("SmsReceiver", e); Log.w("PushReceiver", e);
handleReceivedCorruptedKey(masterSecret, message, true); handleReceivedCorruptedKey(masterSecret, message, true);
} catch (InvalidKeyIdException e) { } catch (InvalidKeyIdException e) {
Log.w("SmsReceiver", e); Log.w("PushReceiver", e);
handleReceivedCorruptedKey(masterSecret, message, false);
} catch (InvalidMessageException e) {
Log.w("PushReceiver", e);
handleReceivedCorruptedKey(masterSecret, message, false); handleReceivedCorruptedKey(masterSecret, message, false);
} }
} }
@ -237,9 +241,7 @@ public class PushReceiver {
placeholder = new IncomingEncryptedMessage(placeholder, ""); placeholder = new IncomingEncryptedMessage(placeholder, "");
} }
Pair<Long, Long> messageAndThreadId = DatabaseFactory.getEncryptingSmsDatabase(context) return DatabaseFactory.getEncryptingSmsDatabase(context)
.insertMessageInbox(masterSecret, .insertMessageInbox(masterSecret, placeholder);
placeholder);
return messageAndThreadId;
} }
} }

View File

@ -23,6 +23,7 @@ import android.util.Pair;
import org.thoughtcrime.securesms.crypto.DecryptingQueue; import org.thoughtcrime.securesms.crypto.DecryptingQueue;
import org.thoughtcrime.securesms.crypto.KeyExchangeProcessor; import org.thoughtcrime.securesms.crypto.KeyExchangeProcessor;
import org.thoughtcrime.securesms.crypto.KeyExchangeProcessorV2;
import org.thoughtcrime.securesms.crypto.MasterSecretUtil; import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
import org.thoughtcrime.securesms.crypto.protocol.KeyExchangeMessage; import org.thoughtcrime.securesms.crypto.protocol.KeyExchangeMessage;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
@ -33,15 +34,17 @@ import org.thoughtcrime.securesms.protocol.WirePrefix;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.sms.IncomingEncryptedMessage; import org.thoughtcrime.securesms.sms.IncomingEncryptedMessage;
import org.thoughtcrime.securesms.sms.IncomingKeyExchangeMessage; import org.thoughtcrime.securesms.sms.IncomingKeyExchangeMessage;
import org.thoughtcrime.securesms.sms.IncomingPreKeyBundleMessage;
import org.thoughtcrime.securesms.sms.IncomingTextMessage; import org.thoughtcrime.securesms.sms.IncomingTextMessage;
import org.thoughtcrime.securesms.sms.MultipartSmsMessageHandler; import org.thoughtcrime.securesms.sms.MultipartSmsMessageHandler;
import org.thoughtcrime.securesms.sms.SmsTransportDetails; import org.thoughtcrime.securesms.sms.SmsTransportDetails;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.textsecure.crypto.InvalidKeyException; import org.whispersystems.textsecure.crypto.InvalidKeyException;
import org.whispersystems.textsecure.crypto.InvalidMessageException;
import org.whispersystems.textsecure.crypto.InvalidVersionException; import org.whispersystems.textsecure.crypto.InvalidVersionException;
import org.whispersystems.textsecure.crypto.MasterSecret; import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage; import org.whispersystems.textsecure.crypto.protocol.PreKeyWhisperMessage;
import org.whispersystems.textsecure.crypto.protocol.PreKeyBundleMessage; import org.whispersystems.textsecure.crypto.protocol.WhisperMessageV2;
import org.whispersystems.textsecure.storage.InvalidKeyIdException; import org.whispersystems.textsecure.storage.InvalidKeyIdException;
import java.io.IOException; import java.io.IOException;
@ -97,26 +100,27 @@ public class SmsReceiver {
} }
} }
private Pair<Long, Long> storePreKeyBundledMessage(MasterSecret masterSecret, private Pair<Long, Long> storePreKeyWhisperMessage(MasterSecret masterSecret,
IncomingKeyExchangeMessage message) IncomingPreKeyBundleMessage message)
{ {
Log.w("SmsReceiver", "Processing prekey message..."); Log.w("SmsReceiver", "Processing prekey message...");
try { try {
Recipient recipient = new Recipient(null, message.getSender(), null, null); Recipient recipient = new Recipient(null, message.getSender(), null, null);
KeyExchangeProcessor processor = new KeyExchangeProcessor(context, masterSecret, recipient); KeyExchangeProcessorV2 processor = new KeyExchangeProcessorV2(context, masterSecret, recipient);
SmsTransportDetails transportDetails = new SmsTransportDetails(); SmsTransportDetails transportDetails = new SmsTransportDetails();
PreKeyBundleMessage preKeyExchange = new PreKeyBundleMessage(transportDetails.getDecodedMessage(message.getMessageBody().getBytes())); byte[] rawMessage = transportDetails.getDecodedMessage(message.getMessageBody().getBytes());
PreKeyWhisperMessage preKeyExchange = new PreKeyWhisperMessage(rawMessage);
if (processor.isTrusted(preKeyExchange)) { if (processor.isTrusted(preKeyExchange)) {
processor.processKeyExchangeMessage(preKeyExchange); processor.processKeyExchangeMessage(preKeyExchange);
CiphertextMessage ciphertextMessage = preKeyExchange.getBundledMessage(); WhisperMessageV2 ciphertextMessage = preKeyExchange.getWhisperMessage();
String bundledMessageBody = new String(transportDetails.getEncodedMessage(ciphertextMessage.serialize())); String bundledMessageBody = new String(transportDetails.getEncodedMessage(ciphertextMessage.serialize()));
IncomingEncryptedMessage bundledMessage = new IncomingEncryptedMessage(message, bundledMessageBody); IncomingEncryptedMessage bundledMessage = new IncomingEncryptedMessage(message, bundledMessageBody);
Pair<Long, Long> messageAndThreadId = storeSecureMessage(masterSecret, bundledMessage); Pair<Long, Long> messageAndThreadId = storeSecureMessage(masterSecret, bundledMessage);
Intent intent = new Intent(KeyExchangeProcessor.SECURITY_UPDATE_EVENT); Intent intent = new Intent(KeyExchangeProcessorV2.SECURITY_UPDATE_EVENT);
intent.putExtra("thread_id", messageAndThreadId.second); intent.putExtra("thread_id", messageAndThreadId.second);
intent.setPackage(context.getPackageName()); intent.setPackage(context.getPackageName());
context.sendBroadcast(intent, KeyCachingService.KEY_PERMISSION); context.sendBroadcast(intent, KeyCachingService.KEY_PERMISSION);
@ -135,6 +139,9 @@ public class SmsReceiver {
} catch (IOException e) { } catch (IOException e) {
Log.w("SmsReceive", e); Log.w("SmsReceive", e);
message.setCorrupted(true); message.setCorrupted(true);
} catch (InvalidMessageException e) {
Log.w("SmsReceiver", e);
message.setCorrupted(true);
} }
return storeStandardMessage(masterSecret, message); return storeStandardMessage(masterSecret, message);
@ -145,19 +152,17 @@ public class SmsReceiver {
{ {
if (masterSecret != null && TextSecurePreferences.isAutoRespondKeyExchangeEnabled(context)) { if (masterSecret != null && TextSecurePreferences.isAutoRespondKeyExchangeEnabled(context)) {
try { try {
Recipient recipient = new Recipient(null, message.getSender(), null, null); Recipient recipient = new Recipient(null, message.getSender(), null, null);
KeyExchangeMessage keyExchangeMessage = new KeyExchangeMessage(message.getMessageBody()); KeyExchangeMessage exchangeMessage = KeyExchangeMessage.createFor(message.getMessageBody());
KeyExchangeProcessor processor = new KeyExchangeProcessor(context, masterSecret, recipient); KeyExchangeProcessor processor = KeyExchangeProcessor.createFor(context, masterSecret, recipient, exchangeMessage);
Log.w("SmsReceiver", "Received key with fingerprint: " + keyExchangeMessage.getPublicKey().getFingerprint()); if (processor.isStale(exchangeMessage)) {
if (processor.isStale(keyExchangeMessage)) {
message.setStale(true); message.setStale(true);
} else if (processor.isTrusted(keyExchangeMessage)) { } else if (processor.isTrusted(exchangeMessage)) {
message.setProcessed(true); message.setProcessed(true);
Pair<Long, Long> messageAndThreadId = storeStandardMessage(masterSecret, message); Pair<Long, Long> messageAndThreadId = storeStandardMessage(masterSecret, message);
processor.processKeyExchangeMessage(keyExchangeMessage, messageAndThreadId.second); processor.processKeyExchangeMessage(exchangeMessage, messageAndThreadId.second);
return messageAndThreadId; return messageAndThreadId;
} }
@ -167,6 +172,9 @@ public class SmsReceiver {
} catch (InvalidKeyException e) { } catch (InvalidKeyException e) {
Log.w("SmsReceiver", e); Log.w("SmsReceiver", e);
message.setCorrupted(true); message.setCorrupted(true);
} catch (InvalidMessageException e) {
Log.w("SmsReceiver", e);
message.setCorrupted(true);
} }
} }
@ -175,7 +183,7 @@ public class SmsReceiver {
private Pair<Long, Long> storeMessage(MasterSecret masterSecret, IncomingTextMessage message) { private Pair<Long, Long> storeMessage(MasterSecret masterSecret, IncomingTextMessage message) {
if (message.isSecureMessage()) return storeSecureMessage(masterSecret, message); if (message.isSecureMessage()) return storeSecureMessage(masterSecret, message);
else if (message.isPreKeyBundle()) return storePreKeyBundledMessage(masterSecret, (IncomingKeyExchangeMessage) message); else if (message.isPreKeyBundle()) return storePreKeyWhisperMessage(masterSecret, (IncomingPreKeyBundleMessage) message);
else if (message.isKeyExchange()) return storeKeyExchangeMessage(masterSecret, (IncomingKeyExchangeMessage) message); else if (message.isKeyExchange()) return storeKeyExchangeMessage(masterSecret, (IncomingKeyExchangeMessage) message);
else return storeStandardMessage(masterSecret, message); else return storeStandardMessage(masterSecret, message);
} }
@ -191,7 +199,7 @@ public class SmsReceiver {
} }
public void process(MasterSecret masterSecret, Intent intent) { public void process(MasterSecret masterSecret, Intent intent) {
if (intent.getAction().equals(SendReceiveService.RECEIVE_SMS_ACTION)) { if (SendReceiveService.RECEIVE_SMS_ACTION.equals(intent.getAction())) {
handleReceiveMessage(masterSecret, intent); handleReceiveMessage(masterSecret, intent);
} }
} }

View File

@ -21,7 +21,6 @@ import android.content.Context;
import android.telephony.TelephonyManager; import android.telephony.TelephonyManager;
import android.util.Log; import android.util.Log;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.mms.MmsRadio; import org.thoughtcrime.securesms.mms.MmsRadio;
import org.thoughtcrime.securesms.mms.MmsRadioException; import org.thoughtcrime.securesms.mms.MmsRadioException;
@ -30,10 +29,8 @@ import org.thoughtcrime.securesms.mms.MmsSendResult;
import org.thoughtcrime.securesms.mms.TextTransport; import org.thoughtcrime.securesms.mms.TextTransport;
import org.thoughtcrime.securesms.protocol.WirePrefix; import org.thoughtcrime.securesms.protocol.WirePrefix;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.whispersystems.textsecure.crypto.IdentityKeyPair;
import org.whispersystems.textsecure.crypto.MasterSecret; import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.crypto.MessageCipher; import org.whispersystems.textsecure.crypto.SessionCipher;
import org.whispersystems.textsecure.crypto.ecc.Curve;
import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage; import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage;
import org.whispersystems.textsecure.util.Hex; import org.whispersystems.textsecure.util.Hex;
@ -158,9 +155,8 @@ public class MmsTransport {
private byte[] getEncryptedPdu(MasterSecret masterSecret, String recipientString, byte[] pduBytes) { private byte[] getEncryptedPdu(MasterSecret masterSecret, String recipientString, byte[] pduBytes) {
TextTransport transportDetails = new TextTransport(); TextTransport transportDetails = new TextTransport();
Recipient recipient = new Recipient(null, recipientString, null, null); Recipient recipient = new Recipient(null, recipientString, null, null);
IdentityKeyPair identityKey = IdentityKeyUtil.getIdentityKeyPair(context, masterSecret, Curve.DJB_TYPE); SessionCipher sessionCipher = SessionCipher.createFor(context, masterSecret, recipient);
MessageCipher messageCipher = new MessageCipher(context, masterSecret, identityKey); CiphertextMessage ciphertextMessage = sessionCipher.encrypt(pduBytes);
CiphertextMessage ciphertextMessage = messageCipher.encrypt(recipient, pduBytes);
return transportDetails.getEncodedMessage(ciphertextMessage.serialize()); return transportDetails.getEncodedMessage(ciphertextMessage.serialize());
} }

View File

@ -24,7 +24,7 @@ import com.google.protobuf.ByteString;
import org.thoughtcrime.securesms.Release; import org.thoughtcrime.securesms.Release;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.crypto.KeyExchangeProcessor; import org.thoughtcrime.securesms.crypto.KeyExchangeProcessorV2;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord; import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.mms.PartParser; import org.thoughtcrime.securesms.mms.PartParser;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
@ -34,14 +34,10 @@ import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.TextSecurePushCredentials; import org.thoughtcrime.securesms.util.TextSecurePushCredentials;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.textsecure.crypto.AttachmentCipher; import org.whispersystems.textsecure.crypto.AttachmentCipher;
import org.whispersystems.textsecure.crypto.IdentityKey; import org.whispersystems.textsecure.crypto.InvalidKeyException;
import org.whispersystems.textsecure.crypto.IdentityKeyPair;
import org.whispersystems.textsecure.crypto.KeyUtil;
import org.whispersystems.textsecure.crypto.MasterSecret; import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.crypto.MessageCipher; import org.whispersystems.textsecure.crypto.SessionCipher;
import org.whispersystems.textsecure.crypto.ecc.Curve;
import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage; import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage;
import org.whispersystems.textsecure.crypto.protocol.PreKeyBundleMessage;
import org.whispersystems.textsecure.push.OutgoingPushMessage; import org.whispersystems.textsecure.push.OutgoingPushMessage;
import org.whispersystems.textsecure.push.PreKeyEntity; import org.whispersystems.textsecure.push.PreKeyEntity;
import org.whispersystems.textsecure.push.PushAttachmentData; import org.whispersystems.textsecure.push.PushAttachmentData;
@ -51,6 +47,7 @@ import org.whispersystems.textsecure.push.PushDestination;
import org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent; import org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent;
import org.whispersystems.textsecure.push.PushServiceSocket; import org.whispersystems.textsecure.push.PushServiceSocket;
import org.whispersystems.textsecure.push.RateLimitException; import org.whispersystems.textsecure.push.RateLimitException;
import org.whispersystems.textsecure.storage.SessionRecordV2;
import java.io.IOException; import java.io.IOException;
import java.util.LinkedList; import java.util.LinkedList;
@ -166,62 +163,27 @@ public class PushTransport extends BaseTransport {
PushDestination pushDestination, byte[] plaintext) PushDestination pushDestination, byte[] plaintext)
throws IOException throws IOException
{ {
if (KeyUtil.isNonPrekeySessionFor(context, masterSecret, recipient)) { if (!SessionRecordV2.hasSession(context, masterSecret, recipient)) {
Log.w("PushTransport", "Sending standard ciphertext message..."); try {
byte[] ciphertext = getEncryptedMessageForExistingSession(recipient, plaintext); PreKeyEntity preKey = socket.getPreKey(pushDestination);
return new PushBody(OutgoingPushMessage.TYPE_MESSAGE_CIPHERTEXT, ciphertext); KeyExchangeProcessorV2 processor = new KeyExchangeProcessorV2(context, masterSecret, recipient);
} else if (KeyUtil.isSessionFor(context, recipient)) {
Log.w("PushTransport", "Sending prekeybundle ciphertext message for existing session..."); processor.processKeyExchangeMessage(preKey, threadId);
byte[] ciphertext = getEncryptedPrekeyBundleMessageForExistingSession(recipient, plaintext); } catch (InvalidKeyException e) {
return new PushBody(OutgoingPushMessage.TYPE_MESSAGE_PREKEY_BUNDLE, ciphertext); Log.w("PushTransport", e);
throw new IOException("Invalid PreKey!");
}
}
SessionCipher cipher = SessionCipher.createFor(context, masterSecret, recipient);
CiphertextMessage message = cipher.encrypt(plaintext);
if (message.getType() == CiphertextMessage.PREKEY_WHISPER_TYPE) {
return new PushBody(OutgoingPushMessage.TYPE_MESSAGE_PREKEY_BUNDLE, message.serialize());
} else if (message.getType() == CiphertextMessage.CURRENT_WHISPER_TYPE) {
return new PushBody(OutgoingPushMessage.TYPE_MESSAGE_CIPHERTEXT, message.serialize());
} else { } else {
Log.w("PushTransport", "Sending prekeybundle ciphertext message for new session..."); throw new AssertionError("Unknown ciphertext type: " + message.getType());
byte[] ciphertext = getEncryptedPrekeyBundleMessageForNewSession(socket, threadId, recipient, pushDestination, plaintext);
return new PushBody(OutgoingPushMessage.TYPE_MESSAGE_PREKEY_BUNDLE, ciphertext);
} }
} }
private byte[] getEncryptedPrekeyBundleMessageForExistingSession(Recipient recipient,
byte[] plaintext)
{
IdentityKeyPair identityKeyPair = IdentityKeyUtil.getIdentityKeyPair(context, masterSecret, Curve.DJB_TYPE);
IdentityKey identityKey = identityKeyPair.getPublicKey();
MessageCipher messageCipher = new MessageCipher(context, masterSecret, identityKeyPair);
CiphertextMessage ciphertextMessage = messageCipher.encrypt(recipient, plaintext);
PreKeyBundleMessage preKeyBundleMessage = new PreKeyBundleMessage(ciphertextMessage, identityKey);
return preKeyBundleMessage.serialize();
}
private byte[] getEncryptedPrekeyBundleMessageForNewSession(PushServiceSocket socket,
long threadId,
Recipient recipient,
PushDestination pushDestination,
byte[] plaintext)
throws IOException
{
IdentityKeyPair identityKeyPair = IdentityKeyUtil.getIdentityKeyPair(context, masterSecret, Curve.DJB_TYPE);
IdentityKey identityKey = identityKeyPair.getPublicKey();
PreKeyEntity preKey = socket.getPreKey(pushDestination);
KeyExchangeProcessor processor = new KeyExchangeProcessor(context, masterSecret, recipient);
processor.processKeyExchangeMessage(preKey, threadId);
MessageCipher messageCipher = new MessageCipher(context, masterSecret, identityKeyPair);
CiphertextMessage ciphertextMessage = messageCipher.encrypt(recipient, plaintext);
PreKeyBundleMessage preKeyBundleMessage = new PreKeyBundleMessage(ciphertextMessage, identityKey);
return preKeyBundleMessage.serialize();
}
private byte[] getEncryptedMessageForExistingSession(Recipient recipient, byte[] plaintext)
throws IOException
{
IdentityKeyPair identityKeyPair = IdentityKeyUtil.getIdentityKeyPair(context, masterSecret, Curve.DJB_TYPE);
MessageCipher messageCipher = new MessageCipher(context, masterSecret, identityKeyPair);
CiphertextMessage ciphertextMessage = messageCipher.encrypt(recipient, plaintext);
return ciphertextMessage.serialize();
}
} }

View File

@ -22,7 +22,6 @@ import android.content.Context;
import android.telephony.SmsManager; import android.telephony.SmsManager;
import android.util.Log; import android.util.Log;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord; import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.service.SendReceiveService; import org.thoughtcrime.securesms.service.SendReceiveService;
@ -32,13 +31,9 @@ import org.thoughtcrime.securesms.sms.OutgoingPrekeyBundleMessage;
import org.thoughtcrime.securesms.sms.OutgoingTextMessage; import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
import org.thoughtcrime.securesms.sms.SmsTransportDetails; import org.thoughtcrime.securesms.sms.SmsTransportDetails;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.textsecure.crypto.IdentityKeyPair;
import org.whispersystems.textsecure.crypto.KeyUtil;
import org.whispersystems.textsecure.crypto.MasterSecret; import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.crypto.MessageCipher; import org.whispersystems.textsecure.crypto.SessionCipher;
import org.whispersystems.textsecure.crypto.ecc.Curve;
import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage; import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage;
import org.whispersystems.textsecure.crypto.protocol.PreKeyBundleMessage;
import java.util.ArrayList; import java.util.ArrayList;
@ -69,7 +64,7 @@ public class SmsTransport extends BaseTransport {
} }
ArrayList<String> messages = multipartMessageHandler.divideMessage(transportMessage); ArrayList<String> messages = multipartMessageHandler.divideMessage(transportMessage);
ArrayList<PendingIntent> sentIntents = constructSentIntents(message.getId(), message.getType(), messages, true); ArrayList<PendingIntent> sentIntents = constructSentIntents(message.getId(), message.getType(), messages, message.isSecure());
ArrayList<PendingIntent> deliveredIntents = constructDeliveredIntents(message.getId(), message.getType(), messages); ArrayList<PendingIntent> deliveredIntents = constructDeliveredIntents(message.getId(), message.getType(), messages);
Log.w("SmsTransport", "Secure divide into message parts: " + messages.size()); Log.w("SmsTransport", "Secure divide into message parts: " + messages.size());
@ -164,30 +159,20 @@ public class SmsTransport extends BaseTransport {
private OutgoingTextMessage getAsymmetricEncrypt(MasterSecret masterSecret, private OutgoingTextMessage getAsymmetricEncrypt(MasterSecret masterSecret,
OutgoingTextMessage message) OutgoingTextMessage message)
{ {
Recipient recipient = message.getRecipients().getPrimaryRecipient(); Recipient recipient = message.getRecipients().getPrimaryRecipient();
String body = message.getMessageBody(); String body = message.getMessageBody();
IdentityKeyPair identityKey = IdentityKeyUtil.getIdentityKeyPair(context, masterSecret, SmsTransportDetails transportDetails = new SmsTransportDetails();
Curve.DJB_TYPE); SessionCipher sessionCipher = SessionCipher.createFor(context, masterSecret, recipient);
byte[] paddedPlaintext = transportDetails.getPaddedMessageBody(body.getBytes());
CiphertextMessage ciphertextMessage = sessionCipher.encrypt(paddedPlaintext);
String encodedCiphertext = new String(transportDetails.getEncodedMessage(ciphertextMessage.serialize()));
SmsTransportDetails transportDetails = new SmsTransportDetails(); if (ciphertextMessage.getType() == CiphertextMessage.PREKEY_WHISPER_TYPE) {
message = new OutgoingPrekeyBundleMessage(message, encodedCiphertext);
if (KeyUtil.isNonPrekeySessionFor(context, masterSecret, recipient)) {
Log.w("SmsTransport", "Delivering standard ciphertext...");
MessageCipher messageCipher = new MessageCipher(context, masterSecret, identityKey);
byte[] paddedPlaintext = transportDetails.getPaddedMessageBody(body.getBytes());
CiphertextMessage ciphertextMessage = messageCipher.encrypt(recipient, paddedPlaintext);
String ciphertxt = new String(transportDetails.getEncodedMessage(ciphertextMessage.serialize()));
return message.withBody(ciphertxt);
} else { } else {
Log.w("SmsTransport", "Delivering prekeybundle ciphertext..."); message = message.withBody(encodedCiphertext);
MessageCipher messageCipher = new MessageCipher(context, masterSecret, identityKey);
CiphertextMessage ciphertextMessage = messageCipher.encrypt(recipient, body.getBytes());
PreKeyBundleMessage preKeyBundleMessage = new PreKeyBundleMessage(ciphertextMessage, identityKey.getPublicKey());
byte[] cipherText = preKeyBundleMessage.serialize();
return new OutgoingPrekeyBundleMessage(message, new String(transportDetails.getEncodedMessage(cipherText)));
} }
return message;
} }
} }