decrypt(ApiEncyptedBoxKey key) {
+ return ask(new EncryptedSessionActor.DecryptPackage(key));
+ }
+}
diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/EncryptedSessionActor.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/EncryptedSessionActor.java
new file mode 100644
index 0000000000..1bfd863751
--- /dev/null
+++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/EncryptedSessionActor.java
@@ -0,0 +1,304 @@
+package im.actor.core.modules.encryption.ratchet;
+
+import java.io.IOException;
+import java.util.ArrayList;
+
+import im.actor.core.api.ApiEncyptedBoxKey;
+import im.actor.core.entity.encryption.PeerSession;
+import im.actor.core.modules.ModuleContext;
+import im.actor.core.modules.encryption.ratchet.entity.PrivateKey;
+import im.actor.core.modules.encryption.ratchet.entity.PublicKey;
+import im.actor.core.modules.ModuleActor;
+import im.actor.core.modules.encryption.ratchet.entity.SessionState;
+import im.actor.runtime.*;
+import im.actor.runtime.actors.ask.AskMessage;
+import im.actor.runtime.promise.Promise;
+import im.actor.runtime.crypto.Curve25519;
+import im.actor.runtime.crypto.IntegrityException;
+import im.actor.runtime.crypto.primitives.util.ByteStrings;
+import im.actor.runtime.storage.KeyValueStorage;
+
+import static im.actor.runtime.promise.Promise.success;
+
+/**
+ * Double Ratchet encryption session
+ * Session is identified by:
+ * 1) Destination User's Id
+ * 2) Own Key Group Id
+ * 3) Own Pre Key Id
+ * 4) Their Key Group Id
+ * 5) Their Pre Key Id
+ *
+ * During actor starting it downloads all required key from Key Manager.
+ * To encrypt/decrypt messages this actor spawns encryption chains.
+ */
+class EncryptedSessionActor extends ModuleActor {
+
+ private final String TAG;
+
+ // No need to keep too much decryption chains as all messages are sequenced. Newer messages
+ // never intentionally use old keys, but there are cases when some messages can be sent with
+ // old encryption keys right after messages with new one. Even when we will kill sequence
+ // new actors can be easily started again with same keys.
+ // TODO: Check if this can cause race condition
+ private final int MAX_DECRYPT_CHAINS = 2;
+
+ //
+ // Key References
+ //
+
+ private final int uid;
+ private final PeerSession session;
+
+ //
+ // Convenience references
+ //
+
+ private KeyManager keyManager;
+ private KeyValueStorage sessionStorage;
+
+ //
+ // Internal State
+ //
+
+ private SessionState sessionState;
+ private EncryptedSessionChain encryptionChain = null;
+ private ArrayList decryptionChains = new ArrayList<>();
+ private boolean isFreezed = false;
+
+ //
+ // Constructors and Methods
+ //
+
+ public EncryptedSessionActor(ModuleContext context, PeerSession session) {
+ super(context);
+ this.TAG = "EncryptionSessionActor#" + session.getUid() + "_" + session.getTheirKeyGroupId();
+ this.uid = session.getUid();
+ this.session = session;
+ }
+
+ @Override
+ public void preStart() {
+ super.preStart();
+ keyManager = context().getEncryption().getKeyManager();
+ sessionStorage = context().getEncryption().getKeyValueStorage();
+
+ sessionState = new SessionState();
+ byte[] data = sessionStorage.loadItem(session.getSid());
+ if (data != null) {
+ try {
+ sessionState = SessionState.fromBytes(data);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ private void saveState() {
+ sessionStorage.addOrUpdateItem(session.getSid(), sessionState.toByteArray());
+ }
+
+ //
+ // Encryption
+ //
+
+ private Promise onEncrypt(final byte[] data) {
+
+ // Stage 2: Pick Encryption Chain
+ // Stage 3: Decrypt
+
+ //
+ // Stage 1: Pick Their Ephemeral key
+ // - After this stage we will have public their key and own private key
+ // for encryption chain
+ //
+ Promise ephemeralKey;
+ if (sessionState.getLatestTheirKey() != null) {
+ ephemeralKey = success(sessionState.getLatestTheirKey());
+ } else {
+ ephemeralKey = keyManager.getUserRandomPreKey(uid, session.getTheirKeyGroupId()).map(key -> {
+ // This can be called only for the first time of sending message in this session
+ // So we need to save ephemeral key and generate new initial private key
+ // for this session
+ sessionState = sessionState.updateKeys(key.getPublicKey());
+ saveState();
+ return key.getPublicKey();
+ });
+ }
+
+ return wrap(ephemeralKey
+ .map(publicKey -> pickEncryptChain())
+ .map(encryptedSessionChain -> encrypt(encryptedSessionChain, data))
+ .map(bytes -> new ApiEncyptedBoxKey(session.getUid(), session.getTheirKeyGroupId(),
+ "curve25519", bytes)));
+ }
+
+ private EncryptedSessionChain pickEncryptChain() {
+
+ // Dispose existing encryption chain if their public keys changes
+ // If not return latest one
+ if (encryptionChain != null) {
+ if (ByteStrings.isEquals(sessionState.getLatestTheirKey(), encryptionChain.getTheirPublicKey()) &&
+ ByteStrings.isEquals(sessionState.getLatestOwnPublicKey(), encryptionChain.getOwnPublicKey())) {
+ return encryptionChain;
+ } else {
+ encryptionChain = null;
+ }
+ }
+
+ encryptionChain = new EncryptedSessionChain(session,
+ sessionState.getLatestOwnPrivateKey(),
+ sessionState.getLatestOwnPublicKey(),
+ sessionState.getLatestTheirKey());
+
+ return encryptionChain;
+ }
+
+ private byte[] encrypt(EncryptedSessionChain chain, byte[] data) {
+ byte[] encrypted;
+ try {
+ encrypted = chain.encrypt(data);
+ } catch (IntegrityException e) {
+ e.printStackTrace();
+ throw new RuntimeException(e);
+ }
+ return encrypted;
+ }
+
+ //
+ // Decryption
+ //
+
+ private Promise onDecrypt(ApiEncyptedBoxKey data) {
+
+ //
+ // Stage 1: Parsing message header
+ // Stage 2: Picking decryption chain
+ // Stage 3: Decryption of message
+ // Stage 4: Saving their ephemeral key
+ //
+
+ byte[] material = data.getEncryptedKey();
+ byte[] senderEphemeralKey = ByteStrings.substring(material, 16, 32);
+ byte[] receiverEphemeralKey = ByteStrings.substring(material, 48, 32);
+
+ return wrap(pickDecryptChain(senderEphemeralKey, receiverEphemeralKey)
+ .map(encryptedSessionChain -> decrypt(encryptedSessionChain, material))
+ .then(r -> {
+ // Update Session State keys
+ if (sessionState.getLatestTheirKey() == null ||
+ ByteStrings.isEquals(sessionState.getLatestTheirKey(), senderEphemeralKey)) {
+ sessionState = sessionState.updateKeys(senderEphemeralKey);
+ saveState();
+ }
+ }));
+ }
+
+
+ private Promise pickDecryptChain(final byte[] theirEphemeralKey, final byte[] ephemeralKey) {
+ EncryptedSessionChain pickedChain = null;
+ for (EncryptedSessionChain c : decryptionChains) {
+ if (ByteStrings.isEquals(c.getOwnPublicKey(), ephemeralKey)) {
+ pickedChain = c;
+ break;
+ }
+ }
+ if (pickedChain != null) {
+ return Promise.success(pickedChain);
+ }
+
+ return findOwnPreKey(ephemeralKey).map(privateKey -> {
+ EncryptedSessionChain chain = new EncryptedSessionChain(session, privateKey,
+ ephemeralKey, theirEphemeralKey);
+ decryptionChains.add(0, chain);
+ if (decryptionChains.size() > MAX_DECRYPT_CHAINS) {
+ decryptionChains.remove(MAX_DECRYPT_CHAINS)
+ .safeErase();
+ }
+ return chain;
+ });
+ }
+
+ private Promise findOwnPreKey(byte[] ephemeralKey) {
+ if (ByteStrings.isEquals(ephemeralKey, sessionState.getLatestOwnPublicKey())) {
+ return Promise.success(sessionState.getLatestOwnPrivateKey());
+ }
+ if (ByteStrings.isEquals(ephemeralKey, sessionState.getPrevOwnPublicKey())) {
+ return Promise.success(sessionState.getPrevOwnPrivateKey());
+ }
+ return context().getEncryption().getKeyManager().getOwnPreKey(ephemeralKey)
+ .map(PrivateKey::getKey);
+ }
+
+ private byte[] decrypt(EncryptedSessionChain chain, byte[] data) {
+ byte[] decrypted;
+ try {
+ decrypted = chain.decrypt(data);
+ } catch (IntegrityException e) {
+ e.printStackTrace();
+ throw new RuntimeException(e);
+ }
+ return decrypted;
+ }
+
+ //
+ // Tools
+ //
+
+ private Promise wrap(Promise promise) {
+ isFreezed = true;
+ return promise.after((r, e) -> {
+ isFreezed = false;
+ unstashAll();
+ });
+ }
+
+ //
+ // Actor Messages
+ //
+
+ @Override
+ public Promise onAsk(Object message) throws Exception {
+ if (message instanceof EncryptPackage) {
+ if (isFreezed) {
+ stash();
+ return null;
+ }
+ return onEncrypt(((EncryptPackage) message).getData());
+ } else if (message instanceof DecryptPackage) {
+ if (isFreezed) {
+ stash();
+ return null;
+ }
+ DecryptPackage decryptPackage = (DecryptPackage) message;
+ return onDecrypt(decryptPackage.getData());
+ } else {
+ return super.onAsk(message);
+ }
+ }
+
+ public static class EncryptPackage implements AskMessage {
+ private byte[] data;
+
+ public EncryptPackage(byte[] data) {
+ this.data = data;
+ }
+
+ public byte[] getData() {
+ return data;
+ }
+ }
+
+ public static class DecryptPackage implements AskMessage {
+
+ private ApiEncyptedBoxKey data;
+
+ public DecryptPackage(ApiEncyptedBoxKey data) {
+ this.data = data;
+ }
+
+ public ApiEncyptedBoxKey getData() {
+ return data;
+ }
+ }
+}
\ No newline at end of file
diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/session/EncryptedSessionChain.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/EncryptedSessionChain.java
similarity index 69%
rename from actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/session/EncryptedSessionChain.java
rename to actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/EncryptedSessionChain.java
index 738a0502ba..fb5478f071 100644
--- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/session/EncryptedSessionChain.java
+++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/EncryptedSessionChain.java
@@ -1,12 +1,8 @@
-package im.actor.core.modules.encryption.session;
-
-import java.util.HashSet;
-import java.util.Random;
+package im.actor.core.modules.encryption.ratchet;
import im.actor.core.entity.encryption.PeerSession;
import im.actor.core.util.RandomUtils;
import im.actor.runtime.Crypto;
-import im.actor.runtime.Log;
import im.actor.runtime.crypto.Curve25519;
import im.actor.runtime.crypto.IntegrityException;
import im.actor.runtime.crypto.box.ActorBox;
@@ -21,16 +17,16 @@ public class EncryptedSessionChain {
private PeerSession session;
private byte[] ownPrivateKey;
+ private byte[] ownPublicKey;
private byte[] theirPublicKey;
- private HashSet receivedCounters;
private int sentCounter;
private byte[] rootChainKey;
- public EncryptedSessionChain(PeerSession session, byte[] ownPrivateKey, byte[] theirPublicKey) {
+ public EncryptedSessionChain(PeerSession session, byte[] ownPrivateKey, byte[] ownPublicKey, byte[] theirPublicKey) {
this.session = session;
this.ownPrivateKey = ownPrivateKey;
+ this.ownPublicKey = ownPublicKey;
this.theirPublicKey = theirPublicKey;
- this.receivedCounters = new HashSet<>();
this.sentCounter = 0;
this.rootChainKey = RatchetRootChainKey.makeRootChainKey(
new RatchetPrivateKey(ownPrivateKey),
@@ -50,9 +46,13 @@ public byte[] getTheirPublicKey() {
return theirPublicKey;
}
+ public byte[] getOwnPublicKey() {
+ return ownPublicKey;
+ }
+
public byte[] decrypt(byte[] data) throws IntegrityException {
- if (data.length < 88) {
+ if (data.length < 80) {
throw new IntegrityException("Data length is too small");
}
@@ -60,40 +60,36 @@ public byte[] decrypt(byte[] data) throws IntegrityException {
// Parsing message header
//
- final int senderKeyGroupId = ByteStrings.bytesToInt(data, 0);
- final long senderEphermalKey0Id = ByteStrings.bytesToLong(data, 4);
- final long receiverEphermalKey0Id = ByteStrings.bytesToLong(data, 12);
- final byte[] senderEphemeralKey = ByteStrings.substring(data, 20, 32);
- final byte[] receiverEphemeralKey = ByteStrings.substring(data, 52, 32);
- final int messageIndex = ByteStrings.bytesToInt(data, 84);
+ final long senderEphermalKey0Id = ByteStrings.bytesToLong(data, 0);
+ final long receiverEphermalKey0Id = ByteStrings.bytesToLong(data, 8);
+ final byte[] senderEphemeralKey = ByteStrings.substring(data, 16, 32);
+ final byte[] receiverEphemeralKey = ByteStrings.substring(data, 48, 32);
+ final int messageIndex = ByteStrings.bytesToInt(data, 80);
//
// Validating header
//
-// if (senderKeyGroupId != session.getPeerKeyGroupId()) {
-// throw new IntegrityException("Incorrect sender key group id");
-// }
-// if (senderEphermalKey0Id != session.getTheirPreKey().getKeyId()) {
-// throw new IntegrityException("Incorrect sender pre key id");
-// }
-// if (receiverEphermalKey0Id != session.getOwnPreKey().getKeyId()) {
-// throw new IntegrityException("Incorrect receiver pre key id");
-// }
-// if (ByteStrings.isEquals(senderEphemeralKey, theirPublicKey)) {
-// throw new IntegrityException("Incorrect sender ephemeral key");
-// }
-// if (ByteStrings.isEquals(receiverEphemeralKey, ownPrivateKey)) {
-// throw new IntegrityException("Incorrect receiver ephemeral key");
-// }
+ if (senderEphermalKey0Id != session.getTheirPreKeyId()) {
+ throw new IntegrityException("Incorrect sender pre key id");
+ }
+ if (receiverEphermalKey0Id != session.getOwnPreKeyId()) {
+ throw new IntegrityException("Incorrect receiver pre key id");
+ }
+ if (!ByteStrings.isEquals(senderEphemeralKey, theirPublicKey)) {
+ throw new IntegrityException("Incorrect sender ephemeral key");
+ }
+ if (!ByteStrings.isEquals(receiverEphemeralKey, ownPublicKey)) {
+ throw new IntegrityException("Incorrect receiver ephemeral key");
+ }
//
// Decryption
//
ActorBoxKey ratchetMessageKey = RatchetMessageKey.buildKey(rootChainKey, messageIndex);
- byte[] header = ByteStrings.substring(data, 0, 88);
- byte[] message = ByteStrings.substring(data, 88, data.length - 88);
+ byte[] header = ByteStrings.substring(data, 0, 84);
+ byte[] message = ByteStrings.substring(data, 84, data.length - 84);
return ActorBox.openBox(header, message, ratchetMessageKey);
}
@@ -102,10 +98,9 @@ public byte[] encrypt(byte[] data) throws IntegrityException {
ActorBoxKey ratchetMessageKey = RatchetMessageKey.buildKey(rootChainKey, messageIndex);
byte[] header = ByteStrings.merge(
- ByteStrings.intToBytes(session.getOwnKeyGroupId()),
ByteStrings.longToBytes(session.getOwnPreKeyId()), /*Alice Initial Ephermal*/
ByteStrings.longToBytes(session.getTheirPreKeyId()), /*Bob Initial Ephermal*/
- Curve25519.keyGenPublic(ownPrivateKey),
+ ownPublicKey,
theirPublicKey,
ByteStrings.intToBytes(messageIndex)); /* Message Index */
@@ -125,7 +120,6 @@ public void safeErase() {
for (int i = 0; i < rootChainKey.length; i++) {
rootChainKey[i] = (byte) RandomUtils.randomId(255);
}
- receivedCounters.clear();
sentCounter = 0;
}
}
\ No newline at end of file
diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/EncryptedUser.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/EncryptedUser.java
new file mode 100644
index 0000000000..8cbf6f4cbd
--- /dev/null
+++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/EncryptedUser.java
@@ -0,0 +1,52 @@
+package im.actor.core.modules.encryption.ratchet;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+import im.actor.core.api.ApiEncyptedBoxKey;
+import im.actor.core.modules.encryption.ratchet.entity.EncryptedUserKeys;
+import im.actor.core.modules.encryption.ratchet.entity.UserKeys;
+import im.actor.runtime.actors.ActorInterface;
+import im.actor.runtime.actors.ActorRef;
+import im.actor.runtime.promise.Promise;
+
+/**
+ * Encrypting shared key for private Secret Chats
+ */
+public class EncryptedUser extends ActorInterface {
+
+ public EncryptedUser(@NotNull ActorRef dest) {
+ super(dest);
+ }
+
+ /**
+ * Encrypting shared key
+ *
+ * @param data shared key for encryption
+ * @return promise of list of encrypted shared key
+ */
+ public Promise encrypt(byte[] data) {
+ return ask(new EncryptedUserActor.EncryptBox(data));
+ }
+
+ /**
+ * Decrypting shared key
+ *
+ * @param senderKeyGroupId sender's key group id
+ * @param keys list of encrypted box keys
+ * @return promise of shared key
+ */
+ public Promise decrypt(int senderKeyGroupId, List keys) {
+ return ask(new EncryptedUserActor.DecryptBox(senderKeyGroupId, keys));
+ }
+
+ /**
+ * Notify about user keys updated for refreshing internal keys cache
+ *
+ * @param updatedUserKeys updated user keys
+ */
+ public void onUserKeysChanged(UserKeys updatedUserKeys) {
+ send(new EncryptedUserActor.KeyGroupUpdated(updatedUserKeys));
+ }
+}
diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/EncryptedUserActor.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/EncryptedUserActor.java
new file mode 100644
index 0000000000..afe391dcc1
--- /dev/null
+++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/EncryptedUserActor.java
@@ -0,0 +1,275 @@
+package im.actor.core.modules.encryption.ratchet;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+
+import im.actor.core.api.ApiEncyptedBoxKey;
+import im.actor.core.entity.encryption.PeerSession;
+import im.actor.core.modules.ModuleContext;
+import im.actor.core.modules.encryption.ratchet.entity.EncryptedUserKeys;
+import im.actor.core.modules.encryption.ratchet.entity.UserKeys;
+import im.actor.core.modules.ModuleActor;
+import im.actor.runtime.*;
+import im.actor.runtime.actors.ask.AskMessage;
+import im.actor.runtime.promise.Promise;
+import im.actor.runtime.crypto.primitives.util.ByteStrings;
+import im.actor.runtime.promise.Promises;
+import im.actor.runtime.promise.PromisesArray;
+
+import static im.actor.runtime.promise.Promise.success;
+
+public class EncryptedUserActor extends ModuleActor {
+
+ private final String TAG;
+
+ private final int uid;
+ private final boolean isOwnUser;
+
+ private int ownKeyGroupId;
+ private UserKeys theirKeys;
+
+ private HashMap activeSessions = new HashMap<>();
+ private HashSet ignoredKeyGroups = new HashSet<>();
+
+ private boolean isFreezed = true;
+
+ public EncryptedUserActor(int uid, ModuleContext context) {
+ super(context);
+ this.uid = uid;
+ this.isOwnUser = myUid() == uid;
+ TAG = "EncryptedUserActor#" + uid;
+ }
+
+ @Override
+ public void preStart() {
+ super.preStart();
+
+ KeyManager keyManager = context().getEncryption().getKeyManager();
+
+ Promises.tuple(
+ keyManager.getOwnIdentity(),
+ keyManager.getUserKeyGroups(uid))
+ .then(res -> {
+ Log.d(TAG, "Loaded initial parameters");
+ ownKeyGroupId = res.getT1().getKeyGroup();
+ theirKeys = res.getT2();
+ if (isOwnUser) {
+ ignoredKeyGroups.add(ownKeyGroupId);
+ }
+ isFreezed = false;
+ unstashAll();
+ })
+ .failure(e -> {
+ Log.w(TAG, "Unable to fetch initial parameters. Freezing encryption with user #" + uid);
+ Log.e(TAG, e);
+ });
+ }
+
+ private Promise doEncrypt(byte[] data) {
+
+ // Stage 1: Loading User Key Groups
+ return wrap(PromisesArray.of(theirKeys.getUserKeysGroups())
+
+ // Stage 1.1: Filtering invalid key groups and own key groups
+ .filter(keysGroup -> !ignoredKeyGroups.contains(keysGroup.getKeyGroupId())
+ && (!(isOwnUser && keysGroup.getKeyGroupId() == ownKeyGroupId)))
+
+ // Stage 2: Pick sessions for encryption
+ .map(keysGroup -> {
+ if (activeSessions.containsKey(keysGroup.getKeyGroupId())) {
+ return success(activeSessions.get(keysGroup.getKeyGroupId()).first());
+ }
+ return getSessionManager()
+ .pickSession(uid, keysGroup.getKeyGroupId())
+ .map(src -> spawnSession(src))
+ .failure(e -> {
+ ignoredKeyGroups.add(keysGroup.getKeyGroupId());
+ });
+ })
+ .filterFailed()
+
+ // Stage 3: Encrypt box_keys
+ .map(s -> s.encrypt(data))
+ .filterFailed()
+
+ // Stage 4: Zip Everything together
+ .zip(src -> new EncryptedUserKeys(uid, src, new HashSet<>(ignoredKeyGroups))));
+ }
+
+ private Promise doDecrypt(int senderKeyGroupId, List keys) {
+
+ //
+ // Picking key
+ //
+ if (ignoredKeyGroups.contains(senderKeyGroupId)) {
+ throw new RuntimeException("This key group is ignored");
+ }
+ ApiEncyptedBoxKey key = null;
+ for (ApiEncyptedBoxKey boxKey : keys) {
+ if (boxKey.getKeyGroupId() == ownKeyGroupId && boxKey.getUsersId() == myUid()) {
+ key = boxKey;
+ break;
+ }
+ }
+ if (key == null) {
+ throw new RuntimeException("Unable to find suitable key group's key");
+ }
+ final ApiEncyptedBoxKey finalKey = key;
+
+ //
+ // Decryption
+ //
+ long senderPreKeyId = ByteStrings.bytesToLong(key.getEncryptedKey(), 0);
+ long receiverPreKeyId = ByteStrings.bytesToLong(key.getEncryptedKey(), 8);
+ if (activeSessions.containsKey(key.getKeyGroupId())) {
+ for (EncryptedSession s : activeSessions.get(senderKeyGroupId).getSessions()) {
+ if (s.getSession().getOwnPreKeyId() == receiverPreKeyId &&
+ s.getSession().getTheirPreKeyId() == senderPreKeyId) {
+ return wrap(s.decrypt(key));
+ }
+ }
+ }
+ return wrap(getSessionManager()
+ .pickSession(uid, senderKeyGroupId, receiverPreKeyId, senderPreKeyId)
+ .flatMap(src -> spawnSession(src).decrypt(finalKey)));
+ }
+
+ private void onKeysUpdated(UserKeys userKeys) {
+ this.theirKeys = userKeys;
+ }
+
+
+ //
+ // Tools
+ //
+
+ private EncryptedSession spawnSession(PeerSession peerSession) {
+ EncryptedSession session = new EncryptedSession(peerSession, context());
+ if (activeSessions.containsKey(peerSession.getTheirKeyGroupId())) {
+ activeSessions.get(peerSession.getTheirKeyGroupId()).getSessions().add(session);
+ } else {
+ ArrayList l = new ArrayList<>();
+ l.add(session);
+ activeSessions.put(peerSession.getTheirKeyGroupId(), new KeyGroupHolder(peerSession.getTheirKeyGroupId(), l));
+ }
+ return session;
+ }
+
+ private Promise wrap(Promise p) {
+ isFreezed = true;
+ p.after((r, e) -> {
+ isFreezed = false;
+ unstashAll();
+ });
+ return p;
+ }
+
+ private SessionManager getSessionManager() {
+ return context().getEncryption().getSessionManager();
+ }
+
+
+ //
+ // Messages
+ //
+
+ @Override
+ public Promise onAsk(Object message) throws Exception {
+ if (isFreezed) {
+ stash();
+ return null;
+ }
+
+ if (message instanceof EncryptBox) {
+ EncryptBox encryptBox = (EncryptBox) message;
+ return doEncrypt(encryptBox.getData());
+ } else if (message instanceof DecryptBox) {
+ DecryptBox decryptBox = (DecryptBox) message;
+ return doDecrypt(decryptBox.getSenderKeyGroupId(), decryptBox.getKeys());
+ } else {
+ return super.onAsk(message);
+ }
+ }
+
+ @Override
+ public void onReceive(Object message) {
+ if (message instanceof KeyGroupUpdated) {
+ if (isFreezed) {
+ stash();
+ return;
+ }
+ onKeysUpdated(((KeyGroupUpdated) message).getUserKeys());
+ } else {
+ super.onReceive(message);
+ }
+ }
+
+ public static class EncryptBox implements AskMessage {
+ private byte[] data;
+
+ public EncryptBox(byte[] data) {
+ this.data = data;
+ }
+
+ public byte[] getData() {
+ return data;
+ }
+ }
+
+ public static class DecryptBox implements AskMessage {
+
+ private int senderKeyGroupId;
+ private List keys;
+
+ public DecryptBox(int senderKeyGroupId, List keys) {
+ this.senderKeyGroupId = senderKeyGroupId;
+ this.keys = keys;
+ }
+
+ public int getSenderKeyGroupId() {
+ return senderKeyGroupId;
+ }
+
+ public List getKeys() {
+ return keys;
+ }
+ }
+
+ public static class KeyGroupUpdated {
+
+ private UserKeys userKeys;
+
+ public KeyGroupUpdated(UserKeys userKeys) {
+ this.userKeys = userKeys;
+ }
+
+ public UserKeys getUserKeys() {
+ return userKeys;
+ }
+ }
+
+ private class KeyGroupHolder {
+
+ private int keyGroupId;
+ private ArrayList sessions;
+
+ public KeyGroupHolder(int keyGroupId, ArrayList sessions) {
+ this.keyGroupId = keyGroupId;
+ this.sessions = sessions;
+ }
+
+ public int getKeyGroupId() {
+ return keyGroupId;
+ }
+
+ public ArrayList getSessions() {
+ return sessions;
+ }
+
+ public EncryptedSession first() {
+ return sessions.get(0);
+ }
+ }
+}
\ No newline at end of file
diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/KeyManager.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/KeyManager.java
new file mode 100644
index 0000000000..d48ed9d25d
--- /dev/null
+++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/KeyManager.java
@@ -0,0 +1,150 @@
+package im.actor.core.modules.encryption.ratchet;
+
+import java.util.List;
+
+import im.actor.core.api.ApiEncryptionKeyGroup;
+import im.actor.core.api.ApiKeyGroupHolder;
+import im.actor.core.api.ApiKeyGroupId;
+import im.actor.core.modules.ModuleContext;
+import im.actor.core.modules.encryption.ratchet.entity.OwnIdentity;
+import im.actor.core.modules.encryption.ratchet.entity.PrivateKey;
+import im.actor.core.modules.encryption.ratchet.entity.PublicKey;
+import im.actor.core.modules.encryption.ratchet.entity.UserKeys;
+import im.actor.runtime.actors.ActorInterface;
+import im.actor.runtime.actors.messages.Void;
+import im.actor.runtime.promise.Promise;
+
+import static im.actor.runtime.actors.ActorSystem.system;
+
+/**
+ * Encryption Key Manager. Used for loading user's keys for encryption/decryption.
+ */
+public class KeyManager extends ActorInterface {
+
+ /**
+ * Default Constructor
+ *
+ * @param context actor context
+ */
+ public KeyManager(ModuleContext context) {
+ super(system().actorOf("encryption/keys", () -> new KeyManagerActor(context)));
+ }
+
+
+ //
+ // Identity
+ //
+
+ /**
+ * Loading Own Identity Key
+ *
+ * @return promise of keys
+ */
+ public Promise getOwnIdentity() {
+ return ask(new KeyManagerActor.FetchOwnKey());
+ }
+
+ /**
+ * Loading user key groups by uid
+ *
+ * @param uid user's id
+ * @return promise of key groups
+ */
+ public Promise getUserKeyGroups(int uid) {
+ return ask(new KeyManagerActor.FetchUserKeys(uid));
+ }
+
+
+ //
+ // Pre Keys
+ //
+
+ /**
+ * Load own random pre key
+ *
+ * @return promise of private key
+ */
+ public Promise getOwnRandomPreKey() {
+ return ask(new KeyManagerActor.FetchOwnRandomPreKey());
+ }
+
+ /**
+ * Load own pre key by key id
+ *
+ * @param id key id
+ * @return promise of private key
+ */
+ public Promise getOwnPreKey(long id) {
+ return ask(new KeyManagerActor.FetchOwnPreKeyById(id));
+ }
+
+ /**
+ * Load own pre key by public key
+ *
+ * @param publicKey public key
+ * @return promise of private key
+ */
+ public Promise getOwnPreKey(byte[] publicKey) {
+ return ask(new KeyManagerActor.FetchOwnPreKeyByPublic(publicKey));
+ }
+
+ /**
+ * Loading random user's pre key from key group
+ *
+ * @param uid user's id
+ * @param keyGroupId key group id
+ * @return promise of public key
+ */
+ public Promise getUserRandomPreKey(int uid, int keyGroupId) {
+ return ask(new KeyManagerActor.FetchUserPreKeyRandom(uid, keyGroupId));
+ }
+
+ /**
+ * Loading user's pre key by pre key id
+ *
+ * @param uid user's id
+ * @param keyGroupId key group id
+ * @param preKeyId pre key id
+ * @return promise of public key
+ */
+ public Promise getUserPreKey(int uid, int keyGroupId, long preKeyId) {
+ return ask(new KeyManagerActor.FetchUserPreKey(uid, keyGroupId, preKeyId));
+ }
+
+ //
+ // Updates
+ //
+
+ /**
+ * Call this when update about new key group added received
+ *
+ * @param uid user's id
+ * @param keyGroup added key group
+ * @return promise of void
+ */
+ public Promise onKeyGroupAdded(int uid, ApiEncryptionKeyGroup keyGroup) {
+ return ask(new KeyManagerActor.PublicKeysGroupAdded(uid, keyGroup));
+ }
+
+ /**
+ * Call this when update about key group removing received
+ *
+ * @param uid user's id
+ * @param gid removed key group id
+ * @return promise of void
+ */
+ public Promise onKeyGroupRemoved(int uid, int gid) {
+ return ask(new KeyManagerActor.PublicKeysGroupRemoved(uid, gid));
+ }
+
+ /**
+ * Call this when you will receive error during encrytped message sending
+ *
+ * @param missed missed key groups
+ * @param obsolete obsolete key groups
+ * @return promise of void
+ */
+ public Promise onKeyGroupDiffReceived(List missed, List obsolete) {
+ return ask(new KeyManagerActor.KeyGroupsDiff(missed, obsolete));
+ }
+}
diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/KeyManagerActor.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/KeyManagerActor.java
similarity index 61%
rename from actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/KeyManagerActor.java
rename to actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/KeyManagerActor.java
index 9a6501c501..71520d36a6 100644
--- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/KeyManagerActor.java
+++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/KeyManagerActor.java
@@ -1,44 +1,45 @@
-package im.actor.core.modules.encryption;
+package im.actor.core.modules.encryption.ratchet;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
+import java.util.List;
import im.actor.core.api.ApiEncryptionKey;
import im.actor.core.api.ApiEncryptionKeyGroup;
import im.actor.core.api.ApiEncryptionKeySignature;
+import im.actor.core.api.ApiKeyGroupHolder;
+import im.actor.core.api.ApiKeyGroupId;
import im.actor.core.api.ApiUserOutPeer;
import im.actor.core.api.rpc.RequestCreateNewKeyGroup;
import im.actor.core.api.rpc.RequestLoadPrePublicKeys;
import im.actor.core.api.rpc.RequestLoadPublicKey;
import im.actor.core.api.rpc.RequestLoadPublicKeyGroups;
import im.actor.core.api.rpc.RequestUploadPreKey;
-import im.actor.core.api.rpc.ResponseCreateNewKeyGroup;
-import im.actor.core.api.rpc.ResponsePublicKeyGroups;
-import im.actor.core.api.rpc.ResponsePublicKeys;
import im.actor.core.api.rpc.ResponseVoid;
import im.actor.core.entity.User;
import im.actor.core.modules.ModuleContext;
-import im.actor.core.modules.encryption.entity.PrivateKeyStorage;
-import im.actor.core.modules.encryption.entity.PrivateKey;
-import im.actor.core.modules.encryption.entity.UserKeys;
-import im.actor.core.modules.encryption.entity.UserKeysGroup;
-import im.actor.core.modules.encryption.entity.PublicKey;
+import im.actor.core.modules.encryption.Configuration;
+import im.actor.core.modules.encryption.ratchet.entity.OwnIdentity;
+import im.actor.core.modules.encryption.ratchet.entity.PrivateKeyStorage;
+import im.actor.core.modules.encryption.ratchet.entity.PrivateKey;
+import im.actor.core.modules.encryption.ratchet.entity.UserKeys;
+import im.actor.core.modules.encryption.ratchet.entity.UserKeysGroup;
+import im.actor.core.modules.encryption.ratchet.entity.PublicKey;
import im.actor.core.modules.ModuleActor;
import im.actor.core.util.RandomUtils;
import im.actor.runtime.Crypto;
import im.actor.runtime.Log;
import im.actor.runtime.Storage;
-import im.actor.runtime.actors.ask.AskIntRequest;
import im.actor.runtime.actors.ask.AskMessage;
-import im.actor.runtime.actors.ask.AskResult;
+import im.actor.runtime.actors.messages.Void;
import im.actor.runtime.collections.ManagedList;
import im.actor.runtime.crypto.Curve25519KeyPair;
-import im.actor.runtime.function.Function;
import im.actor.runtime.promise.Promise;
import im.actor.runtime.crypto.Curve25519;
import im.actor.runtime.crypto.ratchet.RatchetKeySignature;
-import im.actor.runtime.function.Consumer;
+import im.actor.runtime.promise.PromiseTools;
+import im.actor.runtime.promise.Promises;
import im.actor.runtime.promise.PromisesArray;
import im.actor.runtime.function.Tuple2;
import im.actor.runtime.storage.KeyValueStorage;
@@ -50,10 +51,14 @@
*/
public class KeyManagerActor extends ModuleActor {
+ // j2objc workaround
+ private static final Void DUMB = null;
+ private static final ResponseVoid DUMB2 = null;
+
private static final String TAG = "KeyManagerActor";
private KeyValueStorage encryptionKeysStorage;
- private HashMap cachedUserKeys = new HashMap();
+ private HashMap cachedUserKeys = new HashMap<>();
private PrivateKeyStorage ownKeys;
private boolean isReady = false;
@@ -121,21 +126,15 @@ public void preStart() {
.map(PrivateKey.SIGN(ownKeys.getIdentityKey()));
Log.d(TAG, "Creation of new key group");
- api(new RequestCreateNewKeyGroup(identityKey, Configuration.SUPPORTED, keys, signatures)).then(new Consumer() {
- @Override
- public void apply(ResponseCreateNewKeyGroup response) {
- ownKeys = ownKeys.setGroupId(response.getKeyGroupId());
- encryptionKeysStorage.addOrUpdateItem(0, ownKeys.toByteArray());
- onMainKeysReady();
- }
- }).failure(new Consumer() {
- @Override
- public void apply(Exception e) {
- Log.w(TAG, "Keys upload error");
- Log.e(TAG, e);
-
- // Just ignore
- }
+ api(new RequestCreateNewKeyGroup(identityKey, Configuration.SUPPORTED, keys, signatures)).then(response -> {
+ ownKeys = ownKeys.setGroupId(response.getKeyGroupId());
+ encryptionKeysStorage.addOrUpdateItem(0, ownKeys.toByteArray());
+ onMainKeysReady();
+ }).failure(e -> {
+ Log.w(TAG, "Keys upload error");
+ Log.e(TAG, e);
+
+ // Just ignore
});
} else {
onMainKeysReady();
@@ -173,22 +172,16 @@ private void onMainKeysReady() {
pendingEphermalKeys.map(PrivateKey.SIGN(ownKeys.getIdentityKey()));
api(new RequestUploadPreKey(ownKeys.getKeyGroupId(), uploadingKeys, uploadingSignatures))
- .then(new Consumer() {
- @Override
- public void apply(ResponseVoid responseVoid) {
- ownKeys = ownKeys.markAsUploaded(pendingEphermalKeys.toArray(new PrivateKey[pendingEphermalKeys.size()]));
- encryptionKeysStorage.addOrUpdateItem(0, ownKeys.toByteArray());
- onAllKeysReady();
- }
+ .then(responseVoid -> {
+ ownKeys = ownKeys.markAsUploaded(pendingEphermalKeys.toArray(new PrivateKey[pendingEphermalKeys.size()]));
+ encryptionKeysStorage.addOrUpdateItem(0, ownKeys.toByteArray());
+ onAllKeysReady();
})
- .failure(new Consumer() {
- @Override
- public void apply(Exception e) {
- Log.w(TAG, "Ephemeral keys upload error");
- Log.e(TAG, e);
+ .failure(e -> {
+ Log.w(TAG, "Ephemeral keys upload error");
+ Log.e(TAG, e);
- // Ignore. This will freeze all encryption operations.
- }
+ // Ignore. This will freeze all encryption operations.
});
} else {
onAllKeysReady();
@@ -282,26 +275,20 @@ private Promise fetchUserGroups(final int uid) {
}
return api(new RequestLoadPublicKeyGroups(new ApiUserOutPeer(uid, user.getAccessHash())))
- .map(new Function>() {
- @Override
- public ArrayList apply(ResponsePublicKeyGroups response) {
- ArrayList keysGroups = new ArrayList<>();
- for (ApiEncryptionKeyGroup keyGroup : response.getPublicKeyGroups()) {
- UserKeysGroup validatedKeysGroup = validateUserKeysGroup(uid, keyGroup);
- if (validatedKeysGroup != null) {
- keysGroups.add(validatedKeysGroup);
- }
+ .map(response -> {
+ ArrayList keysGroups = new ArrayList<>();
+ for (ApiEncryptionKeyGroup keyGroup : response.getPublicKeyGroups()) {
+ UserKeysGroup validatedKeysGroup = validateUserKeysGroup(uid, keyGroup);
+ if (validatedKeysGroup != null) {
+ keysGroups.add(validatedKeysGroup);
}
- return keysGroups;
}
+ return keysGroups;
})
- .map(new Function, UserKeys>() {
- @Override
- public UserKeys apply(ArrayList userKeysGroups) {
- UserKeys userKeys = new UserKeys(uid, userKeysGroups.toArray(new UserKeysGroup[userKeysGroups.size()]));
- cacheUserKeys(userKeys);
- return userKeys;
- }
+ .map(userKeysGroups -> {
+ UserKeys userKeys1 = new UserKeys(uid, userKeysGroups.toArray(new UserKeysGroup[userKeysGroups.size()]));
+ cacheUserKeys(userKeys1);
+ return userKeys1;
});
}
@@ -320,65 +307,62 @@ private Promise fetchUserPreKey(final int uid, final int keyGroupId,
}
return pickUserGroup(uid, keyGroupId)
- .flatMap(new Function, Promise>() {
- @Override
- public Promise apply(final Tuple2 keysGroup) {
-
- //
- // Searching in cache
- //
-
- for (PublicKey p : keysGroup.getT1().getEphemeralKeys()) {
- if (p.getKeyId() == keyId) {
- return Promise.success(p);
- }
+ .flatMap(keysGroup -> {
+
+ //
+ // Searching in cache
+ //
+
+ for (PublicKey p : keysGroup.getT1().getEphemeralKeys()) {
+ if (p.getKeyId() == keyId) {
+ return Promise.success(p);
}
+ }
- //
- // Downloading pre key
- //
-
- ArrayList