aboutsummaryrefslogtreecommitdiffstats
path: root/src/main/java/org/whispersystems/libaxolotl/groups
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/org/whispersystems/libaxolotl/groups')
-rw-r--r--src/main/java/org/whispersystems/libaxolotl/groups/GroupCipher.java146
-rw-r--r--src/main/java/org/whispersystems/libaxolotl/groups/GroupSessionBuilder.java38
-rw-r--r--src/main/java/org/whispersystems/libaxolotl/groups/ratchet/SenderChainKey.java49
-rw-r--r--src/main/java/org/whispersystems/libaxolotl/groups/ratchet/SenderMessageKey.java38
-rw-r--r--src/main/java/org/whispersystems/libaxolotl/groups/state/SenderKeyRecord.java64
-rw-r--r--src/main/java/org/whispersystems/libaxolotl/groups/state/SenderKeyState.java144
-rw-r--r--src/main/java/org/whispersystems/libaxolotl/groups/state/SenderKeyStore.java6
7 files changed, 485 insertions, 0 deletions
diff --git a/src/main/java/org/whispersystems/libaxolotl/groups/GroupCipher.java b/src/main/java/org/whispersystems/libaxolotl/groups/GroupCipher.java
new file mode 100644
index 00000000..43dac752
--- /dev/null
+++ b/src/main/java/org/whispersystems/libaxolotl/groups/GroupCipher.java
@@ -0,0 +1,146 @@
+package org.whispersystems.libaxolotl.groups;
+
+import org.whispersystems.libaxolotl.DuplicateMessageException;
+import org.whispersystems.libaxolotl.InvalidKeyIdException;
+import org.whispersystems.libaxolotl.InvalidMessageException;
+import org.whispersystems.libaxolotl.LegacyMessageException;
+import org.whispersystems.libaxolotl.NoSessionException;
+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.groups.state.SenderKeyStore;
+import org.whispersystems.libaxolotl.protocol.SenderKeyMessage;
+
+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) throws NoSessionException {
+ synchronized (LOCK) {
+ try {
+ 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();
+ } catch (InvalidKeyIdException e) {
+ throw new NoSessionException(e);
+ }
+ }
+ }
+
+ 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);
+ }
+ }
+
+}
diff --git a/src/main/java/org/whispersystems/libaxolotl/groups/GroupSessionBuilder.java b/src/main/java/org/whispersystems/libaxolotl/groups/GroupSessionBuilder.java
new file mode 100644
index 00000000..8b73484b
--- /dev/null
+++ b/src/main/java/org/whispersystems/libaxolotl/groups/GroupSessionBuilder.java
@@ -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());
+ }
+ }
+}
diff --git a/src/main/java/org/whispersystems/libaxolotl/groups/ratchet/SenderChainKey.java b/src/main/java/org/whispersystems/libaxolotl/groups/ratchet/SenderChainKey.java
new file mode 100644
index 00000000..71375923
--- /dev/null
+++ b/src/main/java/org/whispersystems/libaxolotl/groups/ratchet/SenderChainKey.java
@@ -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);
+ }
+ }
+
+}
diff --git a/src/main/java/org/whispersystems/libaxolotl/groups/ratchet/SenderMessageKey.java b/src/main/java/org/whispersystems/libaxolotl/groups/ratchet/SenderMessageKey.java
new file mode 100644
index 00000000..8808a8e8
--- /dev/null
+++ b/src/main/java/org/whispersystems/libaxolotl/groups/ratchet/SenderMessageKey.java
@@ -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;
+ }
+}
diff --git a/src/main/java/org/whispersystems/libaxolotl/groups/state/SenderKeyRecord.java b/src/main/java/org/whispersystems/libaxolotl/groups/state/SenderKeyRecord.java
new file mode 100644
index 00000000..bb1ba952
--- /dev/null
+++ b/src/main/java/org/whispersystems/libaxolotl/groups/state/SenderKeyRecord.java
@@ -0,0 +1,64 @@
+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.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() throws InvalidKeyIdException {
+ if (!senderKeyStates.isEmpty()) {
+ return senderKeyStates.get(0);
+ } else {
+ throw new InvalidKeyIdException("No key state in record!");
+ }
+ }
+
+ 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();
+ }
+}
diff --git a/src/main/java/org/whispersystems/libaxolotl/groups/state/SenderKeyState.java b/src/main/java/org/whispersystems/libaxolotl/groups/state/SenderKeyState.java
new file mode 100644
index 00000000..80498ce0
--- /dev/null
+++ b/src/main/java/org/whispersystems/libaxolotl/groups/state/SenderKeyState.java
@@ -0,0 +1,144 @@
+package org.whispersystems.libaxolotl.groups.state;
+
+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;
+ }
+}
diff --git a/src/main/java/org/whispersystems/libaxolotl/groups/state/SenderKeyStore.java b/src/main/java/org/whispersystems/libaxolotl/groups/state/SenderKeyStore.java
new file mode 100644
index 00000000..da01b1f3
--- /dev/null
+++ b/src/main/java/org/whispersystems/libaxolotl/groups/state/SenderKeyStore.java
@@ -0,0 +1,6 @@
+package org.whispersystems.libaxolotl.groups.state;
+
+public interface SenderKeyStore {
+ public void storeSenderKey(String senderKeyId, SenderKeyRecord record);
+ public SenderKeyRecord loadSenderKey(String senderKeyId);
+}