package im.actor.core.network.api; import java.io.IOException; import java.util.Random; import im.actor.core.network.ActorApi; import im.actor.core.network.Endpoints; import im.actor.core.network.TrustedKey; import im.actor.core.network.mtp.entity.ProtoMessage; import im.actor.core.network.mtp.entity.ProtoPackage; import im.actor.core.network.mtp.entity.ProtoSerializer; import im.actor.core.network.mtp.entity.ProtoStruct; import im.actor.core.network.mtp.entity.RequestDH; import im.actor.core.network.mtp.entity.RequestGetServerKey; import im.actor.core.network.mtp.entity.RequestStartAuth; import im.actor.core.network.mtp.entity.ResponseDoDH; import im.actor.core.network.mtp.entity.ResponseGetServerKey; import im.actor.core.network.mtp.entity.ResponseStartAuth; import im.actor.runtime.util.ExponentialBackoff; import im.actor.runtime.Crypto; import im.actor.runtime.Log; import im.actor.runtime.Network; import im.actor.runtime.actors.Actor; import im.actor.runtime.actors.ActorRef; import im.actor.runtime.actors.ActorCancellable; import im.actor.runtime.bser.DataInput; import im.actor.runtime.crypto.Cryptos; import im.actor.runtime.crypto.Curve25519; import im.actor.runtime.crypto.Curve25519KeyPair; import im.actor.runtime.crypto.Digest; import im.actor.runtime.crypto.primitives.prf.PRF; import im.actor.runtime.crypto.primitives.util.ByteStrings; import im.actor.runtime.mtproto.Connection; import im.actor.runtime.mtproto.ConnectionCallback; import im.actor.runtime.mtproto.CreateConnectionCallback; // Disabling Bounds checks for speeding up calculations /*-[ #define J2OBJC_DISABLE_ARRAY_BOUND_CHECKS 1 ]-*/ public class AuthKeyActor extends Actor { private static final String TAG = "AuthKeyActor"; private ActorRef parentActor; private Endpoints endpoints; private Connection connection; private int connectionId = 0; private long randomId; private Random random = new Random(); private final ExponentialBackoff exponentialBackoff = new ExponentialBackoff(1000, 30000, 25); private ActorState currentState; private ActorCancellable reconnectCancellable; private void startKeyCreation(Endpoints endpoints) { Log.d(TAG, "startKeyCreation"); if (sender() == null) { return; } if (connection != null) { connection.close(); connection = null; } this.parentActor = sender(); this.endpoints = endpoints; this.randomId = random.nextLong(); this.exponentialBackoff.reset(); goToStartState(); if (reconnectCancellable != null) { reconnectCancellable.cancel(); reconnectCancellable = null; } reconnectCancellable = schedule(new TryCreateConnection(), 0); } // // Message Processing // private void onConnectionStarted() { try { if (currentState == null) { throw new IOException(); } ProtoStruct struct = currentState.sendStartMessage(); byte[] data = new ProtoPackage(0, 0, new ProtoMessage(0, struct.toByteArray())).toByteArray(); connection.post(data, 0, data.length); } catch (Exception e) { e.printStackTrace(); crashConnection(); } } private void onMessage(int connectionId, byte[] data, int offset, int len) { if (connectionId != this.connectionId) { Log.d(TAG, "Too old: ignoring"); return; } ProtoStruct protoStruct; try { DataInput dataInput = new DataInput(data, offset, len); ProtoPackage protoPackage = new ProtoPackage(dataInput); if (protoPackage.getAuthId() != 0) { throw new IOException("AuthId != 0"); } if (protoPackage.getSessionId() != 0) { throw new IOException("Session != 0"); } if (protoPackage.getPayload().getMessageId() != 0) { throw new IOException("MessageId != 0"); } protoStruct = ProtoSerializer.readMessagePayload(protoPackage.getPayload().getPayload()); } catch (IOException e) { e.printStackTrace(); crashConnection(); return; } try { if (currentState == null) { throw new IOException(); } currentState.onMessage(protoStruct); } catch (Exception e) { e.printStackTrace(); crashConnection(); } } private void goToStartState() { goToState(new ActorState() { @Override public ProtoStruct sendStartMessage() throws IOException { Log.d(TAG, "Sending RequestStartAuth"); return new RequestStartAuth(randomId); } @Override public void onMessage(ProtoStruct struct) throws IOException { if (struct instanceof ResponseStartAuth) { Log.d(TAG, "Received ResponseStartAuth"); ResponseStartAuth startAuth = (ResponseStartAuth) struct; if (startAuth.getRandomId() != randomId) { throw new IOException("Incorrect RandomId"); } long[] keys = startAuth.getAvailableKeys(); if (keys.length == 0) { throw new IOException("No keys installed on server. Please, configure your server correctly."); } if (endpoints.getTrustedKeys().length == 0) { gotoKeyDownloadState(keys[0], startAuth.getServerNonce()); } else { for (long l : keys) { for (TrustedKey tk : endpoints.getTrustedKeys()) { if (tk.getKeyId() == l) { if (tk.getKey() != null) { gotoDHState(tk.getKeyId(), tk.getKey(), startAuth.getServerNonce()); } else { gotoKeyDownloadState(tk.getKeyId(), startAuth.getServerNonce()); } return; } } } throw new IOException("No trusted keys found!"); } } else { throw new IOException("Expected: ResponseStartAuth, got: " + struct.getClass().getName()); } } }); } private void gotoKeyDownloadState(final long keyId, final byte[] serverNonce) { goToState(new ActorState() { @Override public ProtoStruct sendStartMessage() throws IOException { Log.d(TAG, "Sending RequestGetServerKey"); return new RequestGetServerKey(keyId); } @Override public void onMessage(ProtoStruct struct) throws IOException { if (struct instanceof ResponseGetServerKey) { Log.d(TAG, "Received ResponseGetServerKey"); ResponseGetServerKey r = (ResponseGetServerKey) struct; if (r.getKeyId() != keyId) { throw new IOException("Incorrect KeyId"); } gotoDHState(keyId, r.getKey(), serverNonce); } else { throw new IOException("Expected: ResponseGetServerKey, got: " + struct.getClass().getName()); } } }); } private void gotoDHState(final long keyId, final byte[] key, final byte[] serverNonce) { final byte[] clientNonce = new byte[32]; Crypto.nextBytes(clientNonce); byte[] keyMaterial = new byte[32]; Crypto.nextBytes(keyMaterial); final Curve25519KeyPair clientKeyPair = Curve25519.keyGen(keyMaterial); goToState(new ActorState() { @Override public ProtoStruct sendStartMessage() throws IOException { Log.d(TAG, "Sending RequestDH"); return new RequestDH(randomId, keyId, clientNonce, clientKeyPair.getPublicKey()); } @Override public void onMessage(ProtoStruct struct) throws IOException { if (struct instanceof ResponseDoDH) { Log.d(TAG, "Received ResponseDoDH"); ResponseDoDH r = (ResponseDoDH) struct; if (r.getRandomId() != randomId) { throw new IOException("Incorrect RandomId"); } PRF combinedPrf = Cryptos.PRF_SHA_STREEBOG_256(); byte[] nonce = ByteStrings.merge(clientNonce, serverNonce); byte[] pre_master_secret = Curve25519.calculateAgreement(clientKeyPair.getPrivateKey(), key); byte[] master_secret = combinedPrf.calculate(pre_master_secret, "master secret", nonce, 256); byte[] verify = combinedPrf.calculate(master_secret, "client finished", nonce, 256); if (!Curve25519.verifySignature(key, verify, r.getVerifySign())) { throw new IOException("Incorrect Signature"); } Digest sha256 = Crypto.createSHA256(); sha256.update(master_secret, 0, master_secret.length); byte[] authIdHash = new byte[32]; sha256.doFinal(authIdHash, 0); long authId = ByteStrings.bytesToLong(authIdHash); Log.d(TAG, "Key successfully created #" + authId); gotoSuccess(master_secret, authId); } else { throw new IOException("Expected: ResponseGetServerKey, got: " + struct.getClass().getName()); } } }); } private void gotoSuccess(byte[] masterKey, long authId) { parentActor.send(new KeyCreated(authId, masterKey)); crashConnectionAndState(); } private void goToState(ActorState state) { currentState = state; if (connection != null) { try { ProtoStruct struct = currentState.sendStartMessage(); byte[] data = new ProtoPackage(0, 0, new ProtoMessage(0, struct.toByteArray())).toByteArray(); connection.post(data, 0, data.length); } catch (IOException e) { e.printStackTrace(); } } } // // Connection Management // private void tryCreateConnection() { Log.d(TAG, "Trying to connect..."); final int currentConnection = ++connectionId; Network.createConnection(currentConnection, ActorApi.MTPROTO_VERSION, ActorApi.API_MAJOR_VERSION, ActorApi.API_MINOR_VERSION, endpoints.fetchEndpoint(false), new ConnectionCallback() { @Override public void onConnectionRedirect(String host, int port, int timeout) { // As Not Supported self().send(new AuthKeyActor.OnConnectionDie(currentConnection)); } @Override public void onMessage(byte[] data, int offset, int len) { self().send(new AuthKeyActor.OnMessage(currentConnection, data, offset, len)); } @Override public void onConnectionDie() { self().send(new AuthKeyActor.OnConnectionDie(currentConnection)); } }, new CreateConnectionCallback() { @Override public void onConnectionCreated(Connection connection) { AuthKeyActor.this.onConnectionCreated(currentConnection, connection); } @Override public void onConnectionCreateError() { self().send(new AuthKeyActor.OnConnectionDie(currentConnection)); } }); } private void onConnectionCreated(int connectionId, Connection connection) { Log.d(TAG, "onConnectionCreated"); if (connectionId != this.connectionId) { connection.close(); Log.d(TAG, "Too old: ignoring"); return; } exponentialBackoff.onSuccess(); this.connection = connection; onConnectionStarted(); } private void onConnectionDie(int connectionId) { Log.d(TAG, "onConnectionDie"); if (connectionId != this.connectionId) { Log.d(TAG, "Too old: ignoring"); return; } crashConnection(); exponentialBackoff.onFailure(); if (currentState != null) { long delay = exponentialBackoff.exponentialWait(); Log.d(TAG, "Trying to recreate connection in " + delay + " ms..."); if (reconnectCancellable != null) { reconnectCancellable.cancel(); reconnectCancellable = null; } reconnectCancellable = schedule(new TryCreateConnection(), delay); } } private void crashConnectionAndState() { Log.d(TAG, "Crashing state..."); currentState = null; crashConnection(); } private void crashConnection() { Log.d(TAG, "Crashing connection"); this.connectionId++; if (this.connection != null) { this.connection.close(); this.connection = null; } } // // Actor Messages // @Override public void onReceive(Object message) { if (message instanceof StartKeyCreation) { startKeyCreation(((StartKeyCreation) message).getEndpoints()); } else if (message instanceof OnConnectionDie) { onConnectionDie(((OnConnectionDie) message).getConnectionId()); } else if (message instanceof OnConnectionCreated) { onConnectionCreated(((OnConnectionCreated) message).getConnectionId(), ((OnConnectionCreated) message).getConnection()); } else if (message instanceof OnMessage) { onMessage(((OnMessage) message).getConnectionId(), ((OnMessage) message).getData(), ((OnMessage) message).getOffset(), ((OnMessage) message).getLength()); } else if (message instanceof TryCreateConnection) { tryCreateConnection(); } else { super.onReceive(message); } } private static class OnConnectionDie { private int connectionId; public OnConnectionDie(int connectionId) { this.connectionId = connectionId; } public int getConnectionId() { return connectionId; } } private static class OnConnectionCreated { private int connectionId; private Connection connection; public OnConnectionCreated(int connectionId, Connection connection) { this.connectionId = connectionId; this.connection = connection; } public int getConnectionId() { return connectionId; } public Connection getConnection() { return connection; } } private static class OnMessage { private int connectionId; private byte[] data; private int offset; private int length; public OnMessage(int connectionId, byte[] data, int offset, int length) { this.connectionId = connectionId; this.data = data; this.offset = offset; this.length = length; } public int getConnectionId() { return connectionId; } public byte[] getData() { return data; } public int getOffset() { return offset; } public int getLength() { return length; } } public static class StartKeyCreation { private Endpoints endpoints; public StartKeyCreation(Endpoints endpoints) { this.endpoints = endpoints; } public Endpoints getEndpoints() { return endpoints; } } public static class KeyCreated { private long authKeyId; private byte[] authKey; public KeyCreated(long authKeyId, byte[] authKey) { this.authKeyId = authKeyId; this.authKey = authKey; } public long getAuthKeyId() { return authKeyId; } public byte[] getAuthKey() { return authKey; } } private class TryCreateConnection { } private interface ActorState { ProtoStruct sendStartMessage() throws IOException; void onMessage(ProtoStruct struct) throws IOException; } }