Initial support for sender keys.

This commit is contained in:
Moxie Marlinspike 2014-09-18 20:30:20 -07:00
parent 54612159be
commit 9a0ed659f7
17 changed files with 5464 additions and 6 deletions

View File

@ -83,4 +83,30 @@ message SignedPreKeyRecordStructure {
message IdentityKeyPairStructure {
optional bytes publicKey = 1;
optional bytes privateKey = 2;
}
message SenderKeyStateStructure {
message SenderChainKey {
optional uint32 iteration = 1;
optional bytes seed = 2;
}
message SenderMessageKey {
optional uint32 iteration = 1;
optional bytes seed = 2;
}
message SenderSigningKey {
optional bytes public = 1;
optional bytes private = 2;
}
optional uint32 senderKeyId = 1;
optional SenderChainKey senderChainKey = 2;
optional SenderSigningKey senderSigningKey = 3;
repeated SenderMessageKey senderMessageKeys = 4;
}
message SenderKeyRecordStructure {
repeated SenderKeyStateStructure senderKeyStates = 1;
}

View File

@ -25,4 +25,17 @@ message KeyExchangeMessage {
optional bytes ratchetKey = 3;
optional bytes identityKey = 4;
optional bytes baseKeySignature = 5;
}
message SenderKeyMessage {
optional uint32 id = 1;
optional uint32 iteration = 2;
optional bytes ciphertext = 3;
}
message SenderKeyDistributionMessage {
optional uint32 id = 1;
optional uint32 iteration = 2;
optional bytes chainKey = 3;
optional bytes signingKey = 4;
}

View File

@ -0,0 +1,139 @@
package org.whispersystems.test.groups;
import android.test.AndroidTestCase;
import android.util.Log;
import org.whispersystems.libaxolotl.DuplicateMessageException;
import org.whispersystems.libaxolotl.InvalidMessageException;
import org.whispersystems.libaxolotl.LegacyMessageException;
import org.whispersystems.libaxolotl.ecc.ECKeyPair;
import org.whispersystems.libaxolotl.groups.GroupCipher;
import org.whispersystems.libaxolotl.groups.GroupSessionBuilder;
import org.whispersystems.libaxolotl.protocol.SenderKeyDistributionMessage;
import org.whispersystems.libaxolotl.util.KeyHelper;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
public class GroupCipherTest extends AndroidTestCase {
public void testBasicEncryptDecrypt()
throws LegacyMessageException, DuplicateMessageException, InvalidMessageException
{
InMemorySenderKeyStore aliceStore = new InMemorySenderKeyStore();
InMemorySenderKeyStore bobStore = new InMemorySenderKeyStore();
GroupSessionBuilder aliceSessionBuilder = new GroupSessionBuilder(aliceStore);
GroupSessionBuilder bobSessionBuilder = new GroupSessionBuilder(bobStore);
GroupCipher aliceGroupCipher = new GroupCipher(aliceStore, "groupWithBobInIt");
GroupCipher bobGroupCipher = new GroupCipher(bobStore, "groupWithBobInIt::aliceUserName");
byte[] aliceSenderKey = KeyHelper.generateSenderKey();
ECKeyPair aliceSenderSigningKey = KeyHelper.generateSenderSigningKey();
int aliceSenderKeyId = KeyHelper.generateSenderKeyId();
SenderKeyDistributionMessage aliceDistributionMessage =
aliceSessionBuilder.process("groupWithBobInIt", aliceSenderKeyId, 0,
aliceSenderKey, aliceSenderSigningKey);
bobSessionBuilder.process("groupWithBobInIt::aliceUserName", aliceDistributionMessage);
byte[] ciphertextFromAlice = aliceGroupCipher.encrypt("smert ze smert".getBytes());
byte[] plaintextFromAlice = bobGroupCipher.decrypt(ciphertextFromAlice);
assertTrue(new String(plaintextFromAlice).equals("smert ze smert"));
}
public void testBasicRatchet()
throws LegacyMessageException, DuplicateMessageException, InvalidMessageException
{
InMemorySenderKeyStore aliceStore = new InMemorySenderKeyStore();
InMemorySenderKeyStore bobStore = new InMemorySenderKeyStore();
GroupSessionBuilder aliceSessionBuilder = new GroupSessionBuilder(aliceStore);
GroupSessionBuilder bobSessionBuilder = new GroupSessionBuilder(bobStore);
GroupCipher aliceGroupCipher = new GroupCipher(aliceStore, "groupWithBobInIt");
GroupCipher bobGroupCipher = new GroupCipher(bobStore, "groupWithBobInIt::aliceUserName");
byte[] aliceSenderKey = KeyHelper.generateSenderKey();
ECKeyPair aliceSenderSigningKey = KeyHelper.generateSenderSigningKey();
int aliceSenderKeyId = KeyHelper.generateSenderKeyId();
SenderKeyDistributionMessage aliceDistributionMessage =
aliceSessionBuilder.process("groupWithBobInIt", aliceSenderKeyId, 0,
aliceSenderKey, aliceSenderSigningKey);
bobSessionBuilder.process("groupWithBobInIt::aliceUserName", aliceDistributionMessage);
byte[] ciphertextFromAlice = aliceGroupCipher.encrypt("smert ze smert".getBytes());
byte[] ciphertextFromAlice2 = aliceGroupCipher.encrypt("smert ze smert2".getBytes());
byte[] ciphertextFromAlice3 = aliceGroupCipher.encrypt("smert ze smert3".getBytes());
byte[] plaintextFromAlice = bobGroupCipher.decrypt(ciphertextFromAlice);
try {
bobGroupCipher.decrypt(ciphertextFromAlice);
throw new AssertionError("Should have ratcheted forward!");
} catch (DuplicateMessageException dme) {
// good
}
byte[] plaintextFromAlice2 = bobGroupCipher.decrypt(ciphertextFromAlice2);
byte[] plaintextFromAlice3 = bobGroupCipher.decrypt(ciphertextFromAlice3);
assertTrue(new String(plaintextFromAlice).equals("smert ze smert"));
assertTrue(new String(plaintextFromAlice2).equals("smert ze smert2"));
assertTrue(new String(plaintextFromAlice3).equals("smert ze smert3"));
}
public void testOutOfOrder()
throws LegacyMessageException, DuplicateMessageException, InvalidMessageException
{
InMemorySenderKeyStore aliceStore = new InMemorySenderKeyStore();
InMemorySenderKeyStore bobStore = new InMemorySenderKeyStore();
GroupSessionBuilder aliceSessionBuilder = new GroupSessionBuilder(aliceStore);
GroupSessionBuilder bobSessionBuilder = new GroupSessionBuilder(bobStore);
GroupCipher aliceGroupCipher = new GroupCipher(aliceStore, "groupWithBobInIt");
GroupCipher bobGroupCipher = new GroupCipher(bobStore, "groupWithBobInIt::aliceUserName");
byte[] aliceSenderKey = KeyHelper.generateSenderKey();
ECKeyPair aliceSenderSigningKey = KeyHelper.generateSenderSigningKey();
int aliceSenderKeyId = KeyHelper.generateSenderKeyId();
SenderKeyDistributionMessage aliceDistributionMessage =
aliceSessionBuilder.process("groupWithBobInIt", aliceSenderKeyId, 0,
aliceSenderKey, aliceSenderSigningKey);
bobSessionBuilder.process("groupWithBobInIt::aliceUserName", aliceDistributionMessage);
ArrayList<byte[]> ciphertexts = new ArrayList<>(100);
for (int i=0;i<100;i++) {
ciphertexts.add(aliceGroupCipher.encrypt("up the punks".getBytes()));
}
while (ciphertexts.size() > 0) {
int index = randomInt() % ciphertexts.size();
byte[] ciphertext = ciphertexts.remove(index);
byte[] plaintext = bobGroupCipher.decrypt(ciphertext);
assertTrue(new String(plaintext).equals("up the punks"));
}
}
private int randomInt() {
try {
return SecureRandom.getInstance("SHA1PRNG").nextInt(Integer.MAX_VALUE);
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
}

View File

@ -0,0 +1,33 @@
package org.whispersystems.test.groups;
import org.whispersystems.libaxolotl.groups.state.SenderKeyRecord;
import org.whispersystems.libaxolotl.groups.state.SenderKeyStore;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class InMemorySenderKeyStore implements SenderKeyStore {
private final Map<String, SenderKeyRecord> store = new HashMap<>();
@Override
public void storeSenderKey(String senderKeyId, SenderKeyRecord record) {
store.put(senderKeyId, record);
}
@Override
public SenderKeyRecord loadSenderKey(String senderKeyId) {
try {
SenderKeyRecord record = store.get(senderKeyId);
if (record == null) {
return new SenderKeyRecord();
} else {
return new SenderKeyRecord(record.serialize());
}
} catch (IOException e) {
throw new AssertionError(e);
}
}
}

View File

@ -0,0 +1,144 @@
package org.whispersystems.libaxolotl.groups;
import android.util.Log;
import org.whispersystems.libaxolotl.DuplicateMessageException;
import org.whispersystems.libaxolotl.InvalidKeyIdException;
import org.whispersystems.libaxolotl.InvalidMessageException;
import org.whispersystems.libaxolotl.LegacyMessageException;
import org.whispersystems.libaxolotl.groups.ratchet.SenderChainKey;
import org.whispersystems.libaxolotl.groups.ratchet.SenderMessageKey;
import org.whispersystems.libaxolotl.groups.state.SenderKeyRecord;
import org.whispersystems.libaxolotl.groups.state.SenderKeyState;
import org.whispersystems.libaxolotl.protocol.SenderKeyMessage;
import org.whispersystems.libaxolotl.groups.state.SenderKeyStore;
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 GroupCipher {
static final Object LOCK = new Object();
private final SenderKeyStore senderKeyStore;
private final String senderKeyId;
public GroupCipher(SenderKeyStore senderKeyStore, String senderKeyId) {
this.senderKeyStore = senderKeyStore;
this.senderKeyId = senderKeyId;
}
public byte[] encrypt(byte[] paddedPlaintext) {
synchronized (LOCK) {
SenderKeyRecord record = senderKeyStore.loadSenderKey(senderKeyId);
SenderKeyState senderKeyState = record.getSenderKeyState();
SenderMessageKey senderKey = senderKeyState.getSenderChainKey().getSenderMessageKey();
byte[] ciphertext = getCipherText(senderKey.getIv(), senderKey.getCipherKey(), paddedPlaintext);
SenderKeyMessage senderKeyMessage = new SenderKeyMessage(senderKeyState.getKeyId(),
senderKey.getIteration(),
ciphertext,
senderKeyState.getSigningKeyPrivate());
senderKeyState.setSenderChainKey(senderKeyState.getSenderChainKey().getNext());
senderKeyStore.storeSenderKey(senderKeyId, record);
return senderKeyMessage.serialize();
}
}
public byte[] decrypt(byte[] senderKeyMessageBytes)
throws LegacyMessageException, InvalidMessageException, DuplicateMessageException
{
synchronized (LOCK) {
try {
SenderKeyRecord record = senderKeyStore.loadSenderKey(senderKeyId);
SenderKeyMessage senderKeyMessage = new SenderKeyMessage(senderKeyMessageBytes);
SenderKeyState senderKeyState = record.getSenderKeyState(senderKeyMessage.getKeyId());
senderKeyMessage.verifySignature(senderKeyState.getSigningKeyPublic());
SenderMessageKey senderKey = getSenderKey(senderKeyState, senderKeyMessage.getIteration());
byte[] plaintext = getPlainText(senderKey.getIv(), senderKey.getCipherKey(), senderKeyMessage.getCipherText());
senderKeyStore.storeSenderKey(senderKeyId, record);
return plaintext;
} catch (org.whispersystems.libaxolotl.InvalidKeyException | InvalidKeyIdException e) {
throw new InvalidMessageException(e);
}
}
}
private SenderMessageKey getSenderKey(SenderKeyState senderKeyState, int iteration)
throws DuplicateMessageException, InvalidMessageException
{
SenderChainKey senderChainKey = senderKeyState.getSenderChainKey();
if (senderChainKey.getIteration() > iteration) {
if (senderKeyState.hasSenderMessageKey(iteration)) {
return senderKeyState.removeSenderMessageKey(iteration);
} else {
throw new DuplicateMessageException("Received message with old counter: " +
senderChainKey.getIteration() + " , " + iteration);
}
}
if (senderChainKey.getIteration() - iteration > 2000) {
throw new InvalidMessageException("Over 2000 messages into the future!");
}
while (senderChainKey.getIteration() < iteration) {
senderKeyState.addSenderMessageKey(senderChainKey.getSenderMessageKey());
senderChainKey = senderChainKey.getNext();
}
senderKeyState.setSenderChainKey(senderChainKey.getNext());
return senderChainKey.getSenderMessageKey();
}
private byte[] getPlainText(byte[] iv, byte[] key, byte[] ciphertext)
throws InvalidMessageException
{
try {
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), ivParameterSpec);
return cipher.doFinal(ciphertext);
} catch (NoSuchAlgorithmException | NoSuchPaddingException | java.security.InvalidKeyException |
InvalidAlgorithmParameterException e)
{
throw new AssertionError(e);
} catch (IllegalBlockSizeException | BadPaddingException e) {
throw new InvalidMessageException(e);
}
}
private byte[] getCipherText(byte[] iv, byte[] key, byte[] plaintext) {
try {
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), ivParameterSpec);
return cipher.doFinal(plaintext);
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidAlgorithmParameterException |
IllegalBlockSizeException | BadPaddingException | java.security.InvalidKeyException e)
{
throw new AssertionError(e);
}
}
}

View File

@ -0,0 +1,38 @@
package org.whispersystems.libaxolotl.groups;
import org.whispersystems.libaxolotl.ecc.ECKeyPair;
import org.whispersystems.libaxolotl.groups.state.SenderKeyRecord;
import org.whispersystems.libaxolotl.groups.state.SenderKeyStore;
import org.whispersystems.libaxolotl.protocol.SenderKeyDistributionMessage;
public class GroupSessionBuilder {
private final SenderKeyStore senderKeyStore;
public GroupSessionBuilder(SenderKeyStore senderKeyStore) {
this.senderKeyStore = senderKeyStore;
}
public void process(String sender, SenderKeyDistributionMessage senderKeyDistributionMessage) {
synchronized (GroupCipher.LOCK) {
SenderKeyRecord senderKeyRecord = senderKeyStore.loadSenderKey(sender);
senderKeyRecord.addSenderKeyState(senderKeyDistributionMessage.getId(),
senderKeyDistributionMessage.getIteration(),
senderKeyDistributionMessage.getChainKey(),
senderKeyDistributionMessage.getSignatureKey());
senderKeyStore.storeSenderKey(sender, senderKeyRecord);
}
}
public SenderKeyDistributionMessage process(String groupId, int keyId, int iteration,
byte[] chainKey, ECKeyPair signatureKey)
{
synchronized (GroupCipher.LOCK) {
SenderKeyRecord senderKeyRecord = senderKeyStore.loadSenderKey(groupId);
senderKeyRecord.setSenderKeyState(keyId, iteration, chainKey, signatureKey);
senderKeyStore.storeSenderKey(groupId, senderKeyRecord);
return new SenderKeyDistributionMessage(keyId, iteration, chainKey, signatureKey.getPublicKey());
}
}
}

View File

@ -0,0 +1,49 @@
package org.whispersystems.libaxolotl.groups.ratchet;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public class SenderChainKey {
private static final byte[] MESSAGE_KEY_SEED = {0x01};
private static final byte[] CHAIN_KEY_SEED = {0x02};
private final int iteration;
private final byte[] chainKey;
public SenderChainKey(int iteration, byte[] chainKey) {
this.iteration = iteration;
this.chainKey = chainKey;
}
public int getIteration() {
return iteration;
}
public SenderMessageKey getSenderMessageKey() {
return new SenderMessageKey(iteration, getDerivative(MESSAGE_KEY_SEED, chainKey));
}
public SenderChainKey getNext() {
return new SenderChainKey(iteration + 1, getDerivative(CHAIN_KEY_SEED, chainKey));
}
public byte[] getSeed() {
return chainKey;
}
private byte[] getDerivative(byte[] seed, byte[] key) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(key, "HmacSHA256"));
return mac.doFinal(seed);
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new AssertionError(e);
}
}
}

View File

@ -0,0 +1,38 @@
package org.whispersystems.libaxolotl.groups.ratchet;
import org.whispersystems.libaxolotl.kdf.HKDFv3;
import org.whispersystems.libaxolotl.util.ByteUtil;
public class SenderMessageKey {
private final int iteration;
private final byte[] iv;
private final byte[] cipherKey;
private final byte[] seed;
public SenderMessageKey(int iteration, byte[] seed) {
byte[] derivative = new HKDFv3().deriveSecrets(seed, "WhisperGroup".getBytes(), 48);
byte[][] parts = ByteUtil.split(derivative, 16, 32);
this.iteration = iteration;
this.seed = seed;
this.iv = parts[0];
this.cipherKey = parts[1];
}
public int getIteration() {
return iteration;
}
public byte[] getIv() {
return iv;
}
public byte[] getCipherKey() {
return cipherKey;
}
public byte[] getSeed() {
return seed;
}
}

View File

@ -0,0 +1,61 @@
package org.whispersystems.libaxolotl.groups.state;
import org.whispersystems.libaxolotl.InvalidKeyIdException;
import org.whispersystems.libaxolotl.ecc.ECKeyPair;
import org.whispersystems.libaxolotl.ecc.ECPublicKey;
import org.whispersystems.libaxolotl.groups.state.SenderKeyState;
import org.whispersystems.libaxolotl.state.StorageProtos;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import static org.whispersystems.libaxolotl.state.StorageProtos.SenderKeyRecordStructure;
public class SenderKeyRecord {
private List<SenderKeyState> senderKeyStates = new LinkedList<>();
public SenderKeyRecord() {}
public SenderKeyRecord(byte[] serialized) throws IOException {
SenderKeyRecordStructure senderKeyRecordStructure = SenderKeyRecordStructure.parseFrom(serialized);
for (StorageProtos.SenderKeyStateStructure structure : senderKeyRecordStructure.getSenderKeyStatesList()) {
this.senderKeyStates.add(new SenderKeyState(structure));
}
}
public SenderKeyState getSenderKeyState() {
return senderKeyStates.get(0);
}
public SenderKeyState getSenderKeyState(int keyId) throws InvalidKeyIdException {
for (SenderKeyState state : senderKeyStates) {
if (state.getKeyId() == keyId) {
return state;
}
}
throw new InvalidKeyIdException("No keys for: " + keyId);
}
public void addSenderKeyState(int id, int iteration, byte[] chainKey, ECPublicKey signatureKey) {
senderKeyStates.add(new SenderKeyState(id, iteration, chainKey, signatureKey));
}
public void setSenderKeyState(int id, int iteration, byte[] chainKey, ECKeyPair signatureKey) {
senderKeyStates.clear();
senderKeyStates.add(new SenderKeyState(id, iteration, chainKey, signatureKey));
}
public byte[] serialize() {
SenderKeyRecordStructure.Builder recordStructure = SenderKeyRecordStructure.newBuilder();
for (SenderKeyState senderKeyState : senderKeyStates) {
recordStructure.addSenderKeyStates(senderKeyState.getStructure());
}
return recordStructure.build().toByteArray();
}
}

View File

@ -0,0 +1,146 @@
package org.whispersystems.libaxolotl.groups.state;
import android.util.Log;
import com.google.protobuf.ByteString;
import org.whispersystems.libaxolotl.InvalidKeyException;
import org.whispersystems.libaxolotl.ecc.Curve;
import org.whispersystems.libaxolotl.ecc.ECKeyPair;
import org.whispersystems.libaxolotl.ecc.ECPrivateKey;
import org.whispersystems.libaxolotl.ecc.ECPublicKey;
import org.whispersystems.libaxolotl.groups.ratchet.SenderChainKey;
import org.whispersystems.libaxolotl.groups.ratchet.SenderMessageKey;
import org.whispersystems.libaxolotl.util.guava.Optional;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import static org.whispersystems.libaxolotl.state.StorageProtos.SenderKeyStateStructure;
public class SenderKeyState {
private SenderKeyStateStructure senderKeyStateStructure;
public SenderKeyState(int id, int iteration, byte[] chainKey, ECPublicKey signatureKey) {
this(id, iteration, chainKey, signatureKey, Optional.<ECPrivateKey>absent());
}
public SenderKeyState(int id, int iteration, byte[] chainKey, ECKeyPair signatureKey) {
this(id, iteration, chainKey, signatureKey.getPublicKey(), Optional.of(signatureKey.getPrivateKey()));
}
private SenderKeyState(int id, int iteration, byte[] chainKey,
ECPublicKey signatureKeyPublic,
Optional<ECPrivateKey> signatureKeyPrivate)
{
SenderKeyStateStructure.SenderChainKey senderChainKeyStructure =
SenderKeyStateStructure.SenderChainKey.newBuilder()
.setIteration(iteration)
.setSeed(ByteString.copyFrom(chainKey))
.build();
SenderKeyStateStructure.SenderSigningKey.Builder signingKeyStructure =
SenderKeyStateStructure.SenderSigningKey.newBuilder()
.setPublic(ByteString.copyFrom(signatureKeyPublic.serialize()));
if (signatureKeyPrivate.isPresent()) {
signingKeyStructure.setPrivate(ByteString.copyFrom(signatureKeyPrivate.get().serialize()));
}
this.senderKeyStateStructure = SenderKeyStateStructure.newBuilder()
.setSenderKeyId(id)
.setSenderChainKey(senderChainKeyStructure)
.setSenderSigningKey(signingKeyStructure)
.build();
}
public SenderKeyState(SenderKeyStateStructure senderKeyStateStructure) {
this.senderKeyStateStructure = senderKeyStateStructure;
}
public int getKeyId() {
return senderKeyStateStructure.getSenderKeyId();
}
public SenderChainKey getSenderChainKey() {
return new SenderChainKey(senderKeyStateStructure.getSenderChainKey().getIteration(),
senderKeyStateStructure.getSenderChainKey().getSeed().toByteArray());
}
public void setSenderChainKey(SenderChainKey chainKey) {
SenderKeyStateStructure.SenderChainKey senderChainKeyStructure =
SenderKeyStateStructure.SenderChainKey.newBuilder()
.setIteration(chainKey.getIteration())
.setSeed(ByteString.copyFrom(chainKey.getSeed()))
.build();
this.senderKeyStateStructure = senderKeyStateStructure.toBuilder()
.setSenderChainKey(senderChainKeyStructure)
.build();
}
public ECPublicKey getSigningKeyPublic() throws InvalidKeyException {
return Curve.decodePoint(senderKeyStateStructure.getSenderSigningKey()
.getPublic()
.toByteArray(), 0);
}
public ECPrivateKey getSigningKeyPrivate() {
return Curve.decodePrivatePoint(senderKeyStateStructure.getSenderSigningKey()
.getPrivate().toByteArray());
}
public boolean hasSenderMessageKey(int iteration) {
for (SenderKeyStateStructure.SenderMessageKey senderMessageKey : senderKeyStateStructure.getSenderMessageKeysList()) {
if (senderMessageKey.getIteration() == iteration) return true;
}
return false;
}
public void addSenderMessageKey(SenderMessageKey senderMessageKey) {
SenderKeyStateStructure.SenderMessageKey senderMessageKeyStructure =
SenderKeyStateStructure.SenderMessageKey.newBuilder()
.setIteration(senderMessageKey.getIteration())
.setSeed(ByteString.copyFrom(senderMessageKey.getSeed()))
.build();
this.senderKeyStateStructure = this.senderKeyStateStructure.toBuilder()
.addSenderMessageKeys(senderMessageKeyStructure)
.build();
}
public SenderMessageKey removeSenderMessageKey(int iteration) {
List<SenderKeyStateStructure.SenderMessageKey> keys = new LinkedList<>(senderKeyStateStructure.getSenderMessageKeysList());
Iterator<SenderKeyStateStructure.SenderMessageKey> iterator = keys.iterator();
SenderKeyStateStructure.SenderMessageKey result = null;
while (iterator.hasNext()) {
SenderKeyStateStructure.SenderMessageKey senderMessageKey = iterator.next();
if (senderMessageKey.getIteration() == iteration) {
result = senderMessageKey;
iterator.remove();
break;
}
}
this.senderKeyStateStructure = this.senderKeyStateStructure.toBuilder()
.clearSenderMessageKeys()
.addAllSenderMessageKeys(keys)
.build();
if (result != null) {
return new SenderMessageKey(result.getIteration(), result.getSeed().toByteArray());
} else {
return null;
}
}
public SenderKeyStateStructure getStructure() {
return senderKeyStateStructure;
}
}

View File

@ -0,0 +1,8 @@
package org.whispersystems.libaxolotl.groups.state;
import org.whispersystems.libaxolotl.groups.state.SenderKeyRecord;
public interface SenderKeyStore {
public void storeSenderKey(String senderKeyId, SenderKeyRecord record);
public SenderKeyRecord loadSenderKey(String senderKeyId);
}

View File

@ -21,8 +21,10 @@ public interface CiphertextMessage {
public static final int UNSUPPORTED_VERSION = 1;
public static final int CURRENT_VERSION = 3;
public static final int WHISPER_TYPE = 2;
public static final int PREKEY_TYPE = 3;
public static final int WHISPER_TYPE = 2;
public static final int PREKEY_TYPE = 3;
public static final int SENDERKEY_TYPE = 4;
public static final int SENDERKEY_DISTRIBUTION_TYPE = 5;
// This should be the worst case (worse than V2). So not always accurate, but good enough for padding.
public static final int ENCRYPTED_MESSAGE_OVERHEAD = 53;

View File

@ -0,0 +1,56 @@
package org.whispersystems.libaxolotl.protocol;
import com.google.protobuf.ByteString;
import org.whispersystems.libaxolotl.ecc.ECPublicKey;
import org.whispersystems.libaxolotl.util.ByteUtil;
public class SenderKeyDistributionMessage implements CiphertextMessage {
private final int id;
private final int iteration;
private final byte[] chainKey;
private final ECPublicKey signatureKey;
private final byte[] serialized;
public SenderKeyDistributionMessage(int id, int iteration, byte[] chainKey, ECPublicKey signatureKey) {
byte[] version = {ByteUtil.intsToByteHighAndLow(CURRENT_VERSION, CURRENT_VERSION)};
this.id = id;
this.iteration = iteration;
this.chainKey = chainKey;
this.signatureKey = signatureKey;
this.serialized = WhisperProtos.SenderKeyDistributionMessage.newBuilder()
.setId(id)
.setIteration(iteration)
.setChainKey(ByteString.copyFrom(chainKey))
.setSigningKey(ByteString.copyFrom(signatureKey.serialize()))
.build().toByteArray();
}
@Override
public byte[] serialize() {
return serialized;
}
@Override
public int getType() {
return SENDERKEY_DISTRIBUTION_TYPE;
}
public int getIteration() {
return iteration;
}
public byte[] getChainKey() {
return chainKey;
}
public ECPublicKey getSignatureKey() {
return signatureKey;
}
public int getId() {
return id;
}
}

View File

@ -0,0 +1,121 @@
package org.whispersystems.libaxolotl.protocol;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import org.whispersystems.libaxolotl.InvalidKeyException;
import org.whispersystems.libaxolotl.InvalidMessageException;
import org.whispersystems.libaxolotl.LegacyMessageException;
import org.whispersystems.libaxolotl.ecc.Curve;
import org.whispersystems.libaxolotl.ecc.ECPrivateKey;
import org.whispersystems.libaxolotl.ecc.ECPublicKey;
import org.whispersystems.libaxolotl.util.ByteUtil;
import java.text.ParseException;
public class SenderKeyMessage implements CiphertextMessage {
private static final int SIGNATURE_LENGTH = 64;
private final int messageVersion;
private final int keyId;
private final int iteration;
private final byte[] ciphertext;
private final byte[] serialized;
public SenderKeyMessage(byte[] serialized) throws InvalidMessageException, LegacyMessageException {
try {
byte[][] messageParts = ByteUtil.split(serialized, 1, serialized.length - 1 - SIGNATURE_LENGTH, SIGNATURE_LENGTH);
byte version = messageParts[0][0];
byte[] message = messageParts[1];
byte[] signature = messageParts[2];
if (ByteUtil.highBitsToInt(version) < 3) {
throw new LegacyMessageException("Legacy message: " + ByteUtil.highBitsToInt(version));
}
if (ByteUtil.highBitsToInt(version) > CURRENT_VERSION) {
throw new InvalidMessageException("Unknown version: " + ByteUtil.highBitsToInt(version));
}
WhisperProtos.SenderKeyMessage senderKeyMessage = WhisperProtos.SenderKeyMessage.parseFrom(message);
if (!senderKeyMessage.hasId() ||
!senderKeyMessage.hasIteration() ||
!senderKeyMessage.hasCiphertext())
{
throw new InvalidMessageException("Incomplete message.");
}
this.serialized = serialized;
this.messageVersion = ByteUtil.highBitsToInt(version);
this.keyId = senderKeyMessage.getId();
this.iteration = senderKeyMessage.getIteration();
this.ciphertext = senderKeyMessage.getCiphertext().toByteArray();
} catch (InvalidProtocolBufferException | ParseException e) {
throw new InvalidMessageException(e);
}
}
public SenderKeyMessage(int keyId, int iteration, byte[] ciphertext, ECPrivateKey signatureKey) {
byte[] version = {ByteUtil.intsToByteHighAndLow(CURRENT_VERSION, CURRENT_VERSION)};
byte[] message = WhisperProtos.SenderKeyMessage.newBuilder()
.setId(keyId)
.setIteration(iteration)
.setCiphertext(ByteString.copyFrom(ciphertext))
.build().toByteArray();
byte[] signature = getSignature(signatureKey, ByteUtil.combine(version, message));
this.serialized = ByteUtil.combine(version, message, signature);
this.messageVersion = CURRENT_VERSION;
this.keyId = keyId;
this.iteration = iteration;
this.ciphertext = ciphertext;
}
public int getKeyId() {
return keyId;
}
public int getIteration() {
return iteration;
}
public byte[] getCipherText() {
return ciphertext;
}
public void verifySignature(ECPublicKey signatureKey)
throws InvalidMessageException
{
try {
byte[][] parts = ByteUtil.split(serialized, serialized.length - SIGNATURE_LENGTH, SIGNATURE_LENGTH);
if (!Curve.verifySignature(signatureKey, parts[0], parts[1])) {
throw new InvalidMessageException("Invalid signature!");
}
} catch (InvalidKeyException e) {
throw new InvalidMessageException(e);
}
}
private byte[] getSignature(ECPrivateKey signatureKey, byte[] serialized) {
try {
return Curve.calculateSignature(signatureKey, serialized);
} catch (InvalidKeyException e) {
throw new AssertionError(e);
}
}
@Override
public byte[] serialize() {
return serialized;
}
@Override
public int getType() {
return CiphertextMessage.SENDERKEY_TYPE;
}
}

View File

@ -116,4 +116,28 @@ public class KeyHelper {
return new SignedPreKeyRecord(signedPreKeyId, System.currentTimeMillis(), keyPair, signature);
}
public static ECKeyPair generateSenderSigningKey() {
return Curve.generateKeyPair();
}
public static byte[] generateSenderKey() {
try {
byte[] key = new byte[32];
SecureRandom.getInstance("SHA1PRNG").nextBytes(key);
return key;
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
public static int generateSenderKeyId() {
try {
return SecureRandom.getInstance("SHA1PRNG").nextInt(Integer.MAX_VALUE);
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
}