mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-23 18:15:22 +00:00
Initial support for sender keys.
This commit is contained in:
parent
54612159be
commit
9a0ed659f7
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user