Switch to CBC mode with a derived IV.

1) Since we're not CPU or space constrained (and are in fact
   padding), and since keystream reuse would be more catastrophic
   than IV reuse without chosen plaintext.
This commit is contained in:
Moxie Marlinspike 2014-07-28 20:56:49 -07:00
parent c375ed8638
commit 741171c49f
10 changed files with 243 additions and 46 deletions

View file

@ -19,6 +19,7 @@ message SessionStructure {
optional uint32 index = 1;
optional bytes cipherKey = 2;
optional bytes macKey = 3;
optional bytes iv = 4;
}
repeated MessageKey messageKeys = 4;

View file

@ -102,7 +102,7 @@ public class SessionCipher {
int previousCounter = sessionState.getPreviousCounter();
int sessionVersion = sessionState.getSessionVersion();
byte[] ciphertextBody = getCiphertext(messageKeys, paddedMessage);
byte[] ciphertextBody = getCiphertext(sessionVersion, messageKeys, paddedMessage);
CiphertextMessage ciphertextMessage = new WhisperMessage(sessionVersion, messageKeys.getMacKey(),
senderEphemeral, chainKey.getIndex(),
previousCounter, ciphertextBody,
@ -226,18 +226,19 @@ public class SessionCipher {
sessionState.getSessionVersion()));
}
int messageVersion = ciphertextMessage.getMessageVersion();
ECPublicKey theirEphemeral = ciphertextMessage.getSenderRatchetKey();
int counter = ciphertextMessage.getCounter();
ChainKey chainKey = getOrCreateChainKey(sessionState, theirEphemeral);
MessageKeys messageKeys = getOrCreateMessageKeys(sessionState, theirEphemeral,
chainKey, counter);
ciphertextMessage.verifyMac(ciphertextMessage.getMessageVersion(),
ciphertextMessage.verifyMac(messageVersion,
sessionState.getRemoteIdentityKey(),
sessionState.getLocalIdentityKey(),
messageKeys.getMacKey());
byte[] plaintext = getPlaintext(messageKeys, ciphertextMessage.getBody());
byte[] plaintext = getPlaintext(messageVersion, messageKeys, ciphertextMessage.getBody());
sessionState.clearUnacknowledgedPreKeyMessage();
@ -304,11 +305,15 @@ public class SessionCipher {
return chainKey.getMessageKeys();
}
private byte[] getCiphertext(MessageKeys messageKeys, byte[] plaintext) {
private byte[] getCiphertext(int version, MessageKeys messageKeys, byte[] plaintext) {
try {
Cipher cipher = getCipher(Cipher.ENCRYPT_MODE,
messageKeys.getCipherKey(),
messageKeys.getCounter());
Cipher cipher;
if (version >= 3) {
cipher = getCipher(Cipher.ENCRYPT_MODE, messageKeys.getCipherKey(), messageKeys.getIv());
} else {
cipher = getCipher(Cipher.ENCRYPT_MODE, messageKeys.getCipherKey(), messageKeys.getCounter());
}
return cipher.doFinal(plaintext);
} catch (IllegalBlockSizeException | BadPaddingException e) {
@ -316,11 +321,16 @@ public class SessionCipher {
}
}
private byte[] getPlaintext(MessageKeys messageKeys, byte[] cipherText) {
private byte[] getPlaintext(int version, MessageKeys messageKeys, byte[] cipherText) {
try {
Cipher cipher = getCipher(Cipher.DECRYPT_MODE,
messageKeys.getCipherKey(),
messageKeys.getCounter());
Cipher cipher;
if (version >= 3) {
cipher = getCipher(Cipher.DECRYPT_MODE, messageKeys.getCipherKey(), messageKeys.getIv());
} else {
cipher = getCipher(Cipher.DECRYPT_MODE, messageKeys.getCipherKey(), messageKeys.getCounter());
}
return cipher.doFinal(cipherText);
} catch (IllegalBlockSizeException | BadPaddingException e) {
throw new AssertionError(e);
@ -344,4 +354,16 @@ public class SessionCipher {
throw new AssertionError(e);
}
}
private Cipher getCipher(int mode, SecretKeySpec key, IvParameterSpec iv) {
try {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(mode, key, iv);
return cipher;
} catch (NoSuchAlgorithmException | NoSuchPaddingException | java.security.InvalidKeyException |
InvalidAlgorithmParameterException e)
{
throw new AssertionError(e);
}
}
}

View file

@ -19,22 +19,32 @@ package org.whispersystems.libaxolotl.kdf;
import org.whispersystems.libaxolotl.util.ByteUtil;
import java.text.ParseException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class DerivedMessageSecrets {
public static final int SIZE = 64;
public static final int SIZE = 80;
private static final int CIPHER_KEY_LENGTH = 32;
private static final int MAC_KEY_LENGTH = 32;
private static final int IV_LENGTH = 16;
private final SecretKeySpec cipherKey;
private final SecretKeySpec macKey;
private final SecretKeySpec cipherKey;
private final SecretKeySpec macKey;
private final IvParameterSpec iv;
public DerivedMessageSecrets(byte[] okm) {
byte[][] keys = ByteUtil.split(okm, CIPHER_KEY_LENGTH, MAC_KEY_LENGTH);
try {
byte[][] keys = ByteUtil.split(okm, CIPHER_KEY_LENGTH, MAC_KEY_LENGTH, IV_LENGTH);
this.cipherKey = new SecretKeySpec(keys[0], "AES");
this.macKey = new SecretKeySpec(keys[1], "HmacSHA256");
this.cipherKey = new SecretKeySpec(keys[0], "AES");
this.macKey = new SecretKeySpec(keys[1], "HmacSHA256");
this.iv = new IvParameterSpec(keys[2]);
} catch (ParseException e) {
throw new AssertionError(e);
}
}
public SecretKeySpec getCipherKey() {
@ -44,4 +54,8 @@ public class DerivedMessageSecrets {
public SecretKeySpec getMacKey() {
return macKey;
}
public IvParameterSpec getIv() {
return iv;
}
}

View file

@ -59,7 +59,7 @@ public class ChainKey {
byte[] keyMaterialBytes = kdf.deriveSecrets(inputKeyMaterial, "WhisperMessageKeys".getBytes(), DerivedMessageSecrets.SIZE);
DerivedMessageSecrets keyMaterial = new DerivedMessageSecrets(keyMaterialBytes);
return new MessageKeys(keyMaterial.getCipherKey(), keyMaterial.getMacKey(), index);
return new MessageKeys(keyMaterial.getCipherKey(), keyMaterial.getMacKey(), keyMaterial.getIv(), index);
}
private byte[] getBaseMaterial(byte[] seed) {

View file

@ -16,17 +16,20 @@
*/
package org.whispersystems.libaxolotl.ratchet;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class MessageKeys {
private final SecretKeySpec cipherKey;
private final SecretKeySpec macKey;
private final int counter;
private final SecretKeySpec cipherKey;
private final SecretKeySpec macKey;
private final IvParameterSpec iv;
private final int counter;
public MessageKeys(SecretKeySpec cipherKey, SecretKeySpec macKey, int counter) {
public MessageKeys(SecretKeySpec cipherKey, SecretKeySpec macKey, IvParameterSpec iv, int counter) {
this.cipherKey = cipherKey;
this.macKey = macKey;
this.iv = iv;
this.counter = counter;
}
@ -38,6 +41,10 @@ public class MessageKeys {
return macKey;
}
public IvParameterSpec getIv() {
return iv;
}
public int getCounter() {
return counter;
}

View file

@ -41,6 +41,7 @@ import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import static org.whispersystems.libaxolotl.state.StorageProtos.SessionStructure;
@ -298,6 +299,7 @@ public class SessionState {
if (messageKey.getIndex() == counter) {
result = new MessageKeys(new SecretKeySpec(messageKey.getCipherKey().toByteArray(), "AES"),
new SecretKeySpec(messageKey.getMacKey().toByteArray(), "HmacSHA256"),
new IvParameterSpec(messageKey.getIv().toByteArray()),
messageKey.getIndex());
messageKeyIterator.remove();
@ -323,6 +325,7 @@ public class SessionState {
.setCipherKey(ByteString.copyFrom(messageKeys.getCipherKey().getEncoded()))
.setMacKey(ByteString.copyFrom(messageKeys.getMacKey().getEncoded()))
.setIndex(messageKeys.getCounter())
.setIv(ByteString.copyFrom(messageKeys.getIv().getIV()))
.build();
Chain updatedChain = chain.toBuilder()

View file

@ -1051,6 +1051,16 @@ public final class StorageProtos {
* <code>optional bytes macKey = 3;</code>
*/
com.google.protobuf.ByteString getMacKey();
// optional bytes iv = 4;
/**
* <code>optional bytes iv = 4;</code>
*/
boolean hasIv();
/**
* <code>optional bytes iv = 4;</code>
*/
com.google.protobuf.ByteString getIv();
}
/**
* Protobuf type {@code textsecure.SessionStructure.Chain.MessageKey}
@ -1118,6 +1128,11 @@ public final class StorageProtos {
macKey_ = input.readBytes();
break;
}
case 34: {
bitField0_ |= 0x00000008;
iv_ = input.readBytes();
break;
}
}
}
} catch (com.google.protobuf.InvalidProtocolBufferException e) {
@ -1206,10 +1221,27 @@ public final class StorageProtos {
return macKey_;
}
// optional bytes iv = 4;
public static final int IV_FIELD_NUMBER = 4;
private com.google.protobuf.ByteString iv_;
/**
* <code>optional bytes iv = 4;</code>
*/
public boolean hasIv() {
return ((bitField0_ & 0x00000008) == 0x00000008);
}
/**
* <code>optional bytes iv = 4;</code>
*/
public com.google.protobuf.ByteString getIv() {
return iv_;
}
private void initFields() {
index_ = 0;
cipherKey_ = com.google.protobuf.ByteString.EMPTY;
macKey_ = com.google.protobuf.ByteString.EMPTY;
iv_ = com.google.protobuf.ByteString.EMPTY;
}
private byte memoizedIsInitialized = -1;
public final boolean isInitialized() {
@ -1232,6 +1264,9 @@ public final class StorageProtos {
if (((bitField0_ & 0x00000004) == 0x00000004)) {
output.writeBytes(3, macKey_);
}
if (((bitField0_ & 0x00000008) == 0x00000008)) {
output.writeBytes(4, iv_);
}
getUnknownFields().writeTo(output);
}
@ -1253,6 +1288,10 @@ public final class StorageProtos {
size += com.google.protobuf.CodedOutputStream
.computeBytesSize(3, macKey_);
}
if (((bitField0_ & 0x00000008) == 0x00000008)) {
size += com.google.protobuf.CodedOutputStream
.computeBytesSize(4, iv_);
}
size += getUnknownFields().getSerializedSize();
memoizedSerializedSize = size;
return size;
@ -1375,6 +1414,8 @@ public final class StorageProtos {
bitField0_ = (bitField0_ & ~0x00000002);
macKey_ = com.google.protobuf.ByteString.EMPTY;
bitField0_ = (bitField0_ & ~0x00000004);
iv_ = com.google.protobuf.ByteString.EMPTY;
bitField0_ = (bitField0_ & ~0x00000008);
return this;
}
@ -1415,6 +1456,10 @@ public final class StorageProtos {
to_bitField0_ |= 0x00000004;
}
result.macKey_ = macKey_;
if (((from_bitField0_ & 0x00000008) == 0x00000008)) {
to_bitField0_ |= 0x00000008;
}
result.iv_ = iv_;
result.bitField0_ = to_bitField0_;
onBuilt();
return result;
@ -1440,6 +1485,9 @@ public final class StorageProtos {
if (other.hasMacKey()) {
setMacKey(other.getMacKey());
}
if (other.hasIv()) {
setIv(other.getIv());
}
this.mergeUnknownFields(other.getUnknownFields());
return this;
}
@ -1572,6 +1620,42 @@ public final class StorageProtos {
return this;
}
// optional bytes iv = 4;
private com.google.protobuf.ByteString iv_ = com.google.protobuf.ByteString.EMPTY;
/**
* <code>optional bytes iv = 4;</code>
*/
public boolean hasIv() {
return ((bitField0_ & 0x00000008) == 0x00000008);
}
/**
* <code>optional bytes iv = 4;</code>
*/
public com.google.protobuf.ByteString getIv() {
return iv_;
}
/**
* <code>optional bytes iv = 4;</code>
*/
public Builder setIv(com.google.protobuf.ByteString value) {
if (value == null) {
throw new NullPointerException();
}
bitField0_ |= 0x00000008;
iv_ = value;
onChanged();
return this;
}
/**
* <code>optional bytes iv = 4;</code>
*/
public Builder clearIv() {
bitField0_ = (bitField0_ & ~0x00000008);
iv_ = getDefaultInstance().getIv();
onChanged();
return this;
}
// @@protoc_insertion_point(builder_scope:textsecure.SessionStructure.Chain.MessageKey)
}
@ -8249,7 +8333,7 @@ public final class StorageProtos {
static {
java.lang.String[] descriptorData = {
"\n\032LocalStorageProtocol.proto\022\ntextsecure" +
"\"\307\010\n\020SessionStructure\022\026\n\016sessionVersion\030" +
"\"\323\010\n\020SessionStructure\022\026\n\016sessionVersion\030" +
"\001 \001(\r\022\033\n\023localIdentityPublic\030\002 \001(\014\022\034\n\024re" +
"moteIdentityPublic\030\003 \001(\014\022\017\n\007rootKey\030\004 \001(" +
"\014\022\027\n\017previousCounter\030\005 \001(\r\0227\n\013senderChai" +
@ -8261,33 +8345,33 @@ public final class StorageProtos {
"\001(\0132*.textsecure.SessionStructure.Pendin" +
"gPreKey\022\034\n\024remoteRegistrationId\030\n \001(\r\022\033\n" +
"\023localRegistrationId\030\013 \001(\r\022\024\n\014needsRefre" +
"sh\030\014 \001(\010\022\024\n\014aliceBaseKey\030\r \001(\014\032\255\002\n\005Chain" +
"sh\030\014 \001(\010\022\024\n\014aliceBaseKey\030\r \001(\014\032\271\002\n\005Chain" +
"\022\030\n\020senderRatchetKey\030\001 \001(\014\022\037\n\027senderRatc" +
"hetKeyPrivate\030\002 \001(\014\022=\n\010chainKey\030\003 \001(\0132+." +
"textsecure.SessionStructure.Chain.ChainK" +
"ey\022B\n\013messageKeys\030\004 \003(\0132-.textsecure.Ses" +
"sionStructure.Chain.MessageKey\032&\n\010ChainK" +
"ey\022\r\n\005index\030\001 \001(\r\022\013\n\003key\030\002 \001(\014\032>\n\nMessag",
"ey\022\r\n\005index\030\001 \001(\r\022\013\n\003key\030\002 \001(\014\032J\n\nMessag",
"eKey\022\r\n\005index\030\001 \001(\r\022\021\n\tcipherKey\030\002 \001(\014\022\016" +
"\n\006macKey\030\003 \001(\014\032\315\001\n\022PendingKeyExchange\022\020\n" +
"\010sequence\030\001 \001(\r\022\024\n\014localBaseKey\030\002 \001(\014\022\033\n" +
"\023localBaseKeyPrivate\030\003 \001(\014\022\027\n\017localRatch" +
"etKey\030\004 \001(\014\022\036\n\026localRatchetKeyPrivate\030\005 " +
"\001(\014\022\030\n\020localIdentityKey\030\007 \001(\014\022\037\n\027localId" +
"entityKeyPrivate\030\010 \001(\014\032J\n\rPendingPreKey\022" +
"\020\n\010preKeyId\030\001 \001(\r\022\026\n\016signedPreKeyId\030\003 \001(" +
"\005\022\017\n\007baseKey\030\002 \001(\014\"\177\n\017RecordStructure\0224\n" +
"\016currentSession\030\001 \001(\0132\034.textsecure.Sessi",
"onStructure\0226\n\020previousSessions\030\002 \003(\0132\034." +
"textsecure.SessionStructure\"J\n\025PreKeyRec" +
"ordStructure\022\n\n\002id\030\001 \001(\r\022\021\n\tpublicKey\030\002 " +
"\001(\014\022\022\n\nprivateKey\030\003 \001(\014\"v\n\033SignedPreKeyR" +
"ecordStructure\022\n\n\002id\030\001 \001(\r\022\021\n\tpublicKey\030" +
"\002 \001(\014\022\022\n\nprivateKey\030\003 \001(\014\022\021\n\tsignature\030\004" +
" \001(\014\022\021\n\ttimestamp\030\005 \001(\006\"A\n\030IdentityKeyPa" +
"irStructure\022\021\n\tpublicKey\030\001 \001(\014\022\022\n\nprivat" +
"eKey\030\002 \001(\014B4\n#org.whispersystems.libaxol" +
"otl.stateB\rStorageProtos"
"\n\006macKey\030\003 \001(\014\022\n\n\002iv\030\004 \001(\014\032\315\001\n\022PendingKe" +
"yExchange\022\020\n\010sequence\030\001 \001(\r\022\024\n\014localBase" +
"Key\030\002 \001(\014\022\033\n\023localBaseKeyPrivate\030\003 \001(\014\022\027" +
"\n\017localRatchetKey\030\004 \001(\014\022\036\n\026localRatchetK" +
"eyPrivate\030\005 \001(\014\022\030\n\020localIdentityKey\030\007 \001(" +
"\014\022\037\n\027localIdentityKeyPrivate\030\010 \001(\014\032J\n\rPe" +
"ndingPreKey\022\020\n\010preKeyId\030\001 \001(\r\022\026\n\016signedP" +
"reKeyId\030\003 \001(\005\022\017\n\007baseKey\030\002 \001(\014\"\177\n\017Record" +
"Structure\0224\n\016currentSession\030\001 \001(\0132\034.text",
"secure.SessionStructure\0226\n\020previousSessi" +
"ons\030\002 \003(\0132\034.textsecure.SessionStructure\"" +
"J\n\025PreKeyRecordStructure\022\n\n\002id\030\001 \001(\r\022\021\n\t" +
"publicKey\030\002 \001(\014\022\022\n\nprivateKey\030\003 \001(\014\"v\n\033S" +
"ignedPreKeyRecordStructure\022\n\n\002id\030\001 \001(\r\022\021" +
"\n\tpublicKey\030\002 \001(\014\022\022\n\nprivateKey\030\003 \001(\014\022\021\n" +
"\tsignature\030\004 \001(\014\022\021\n\ttimestamp\030\005 \001(\006\"A\n\030I" +
"dentityKeyPairStructure\022\021\n\tpublicKey\030\001 \001" +
"(\014\022\022\n\nprivateKey\030\002 \001(\014B4\n#org.whispersys" +
"tems.libaxolotl.stateB\rStorageProtos"
};
com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner =
new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() {
@ -8317,7 +8401,7 @@ public final class StorageProtos {
internal_static_textsecure_SessionStructure_Chain_MessageKey_fieldAccessorTable = new
com.google.protobuf.GeneratedMessage.FieldAccessorTable(
internal_static_textsecure_SessionStructure_Chain_MessageKey_descriptor,
new java.lang.String[] { "Index", "CipherKey", "MacKey", });
new java.lang.String[] { "Index", "CipherKey", "MacKey", "Iv", });
internal_static_textsecure_SessionStructure_PendingKeyExchange_descriptor =
internal_static_textsecure_SessionStructure_descriptor.getNestedTypes().get(1);
internal_static_textsecure_SessionStructure_PendingKeyExchange_fieldAccessorTable = new

View file

@ -0,0 +1,57 @@
package org.whispersystems.textsecure.push;
import android.test.AndroidTestCase;
import android.util.Base64;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;
public class PushTransportDetailsTest extends AndroidTestCase {
private final PushTransportDetails transportV2 = new PushTransportDetails(2);
private final PushTransportDetails transportV3 = new PushTransportDetails(3);
public void testV3Padding() {
for (int i=0;i<159;i++) {
byte[] message = new byte[i];
assertEquals(transportV3.getPaddedMessageBody(message).length, 159);
}
for (int i=159;i<319;i++) {
byte[] message = new byte[i];
assertEquals(transportV3.getPaddedMessageBody(message).length, 319);
}
for (int i=319;i<479;i++) {
byte[] message = new byte[i];
assertEquals(transportV3.getPaddedMessageBody(message).length, 479);
}
}
public void testV2Padding() {
for (int i=0;i<480;i++) {
byte[] message = new byte[i];
assertTrue(transportV2.getPaddedMessageBody(message).length == message.length);
}
}
public void testV3Encoding() throws NoSuchAlgorithmException {
byte[] message = new byte[501];
SecureRandom.getInstance("SHA1PRNG").nextBytes(message);
byte[] padded = transportV3.getEncodedMessage(message);
assertTrue(Arrays.equals(padded, message));
}
public void testV2Encoding() throws NoSuchAlgorithmException {
byte[] message = new byte[501];
SecureRandom.getInstance("SHA1PRNG").nextBytes(message);
byte[] padded = transportV2.getEncodedMessage(message);
assertTrue(Arrays.equals(padded, message));
}
}

View file

@ -48,6 +48,12 @@ android {
assets.srcDirs = ['assets']
jniLibs.srcDirs = ['libs']
}
androidTest {
java.srcDirs = ['androidTest/java']
resources.srcDirs = ['androidTest/java']
aidl.srcDirs = ['androidTest/java']
renderscript.srcDirs = ['androidTest/java']
}
}
}
}

View file

@ -58,7 +58,10 @@ public class PushTransportDetails implements TransportDetails {
if (messageVersion < 2) throw new AssertionError("Unknown version: " + messageVersion);
else if (messageVersion == 2) return messageBody;
byte[] paddedMessage = new byte[getPaddedMessageLength(messageBody.length)];
// NOTE: This is dumb. We have our own padding scheme, but so does the cipher.
// The +1 -1 here is to make sure the Cipher has room to add one padding byte,
// otherwise it'll add a full 16 extra bytes.
byte[] paddedMessage = new byte[getPaddedMessageLength(messageBody.length + 1) - 1];
System.arraycopy(messageBody, 0, paddedMessage, 0, messageBody.length);
paddedMessage[messageBody.length] = (byte)0x80;