Search This Blog

Tuesday, June 21, 2016

Authenticated and Encrypted RPC

This example shows how to implement Remote Procedure Calls which are authenticated and encrypted individually for each user and connection session.
The example also shows how to retrieve the user name from within the called method.

The example can be downloaded here.

Introduction

This is a simple example showing how to implement a client-service communication using RPC (Remote Procedure Call) which is authenticated and encrypted for each connected user and session (one user can have multiple connections at the same time).
The authentication is based on SRP (Secure Remote Password) protocol.

To Run Example

  1. Download this example and open it in Visual Studio.
  2. Download Eneter Messaging Framework for .NET platforms or get the nuget package.
    (If your Visual Studio supports Nuget packages then it will be downloaded automatically when you compile.)
  3. Download Eneter Secure Remote Password or get the nuget package.
    (If your Visual Studio supports Nuget packages then it will be downloaded automatically when you compile.)
  4. Compile projects and run the service and then the client application.
  5. Login with the name Peter and the password pwd123.





Service Application

The service is a simple console application which uses RPC to expose an interface and uses the SRP protocol to authenticate connecting clients. Then once the client is authenticated it uses the calculated secret key to encrypt the communication. The key is unique for each connection.
The service implementation also shows how to get the name of the user who calls the remote method. See the method Sum in the Calculator class.
using Eneter.Messaging.DataProcessing.Serializing;
using Eneter.Messaging.Diagnostic;
using Eneter.Messaging.EndPoints.Rpc;
using Eneter.Messaging.MessagingSystems.Composites.AuthenticatedConnection;
using Eneter.Messaging.MessagingSystems.ConnectionProtocols;
using Eneter.Messaging.MessagingSystems.MessagingSystemBase;
using Eneter.Messaging.MessagingSystems.TcpMessagingSystem;
using Eneter.SecureRemotePassword;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Threading;

namespace Service
{
    // Interface declaring the RPC service.
    public interface ICalculator
    {
        double Sum(double a, double b);
    }

    // Implementation of the RPC service.
    internal class Calculator : ICalculator
    {
        public double Sum(double a, double b)
        {
            double aResult = a + b;

            // Example how to get a user name from within the method.
            // Note: it must be called from the same thread as
            // this method was originally called.
            string aUserName = SrpAuthentication.GetUserName();

            // Didplay results.
            Console.WriteLine("{0}: {1} + {2} = {3}", aUserName, a, b, aResult);

            return aResult;
        }
    }

    // Simulates database of users.
    internal static class UserDb
    {
        // Reperesents user authentication data stored in DB.
        public class User
        {
            public User(string userName, byte[] salt, byte[] verifier)
            {
                UserName = userName;
                Salt = salt;
                Verifier = verifier;
            }

            public string UserName { get; private set; }
            public byte[] Salt { get; private set; }
            public byte[] Verifier { get; private set; }
        }

        private static HashSet<User> myUsers = new HashSet<User>();

        public static void CreateUser(string userName, string password)
        {
            // Generate the random salt.
            byte[] s = SRP.s();

            // Compute private key from password nad salt.
            byte[] x = SRP.x(password, s);

            // Compute verifier.
            byte[] v = SRP.v(x);

            // Store user name, salt and the verifier.
            // Note: do not store password nor the private key in database!
            User aUser = new User(userName, s, v);
            lock (myUsers)
            {
                myUsers.Add(aUser);
            }
        }

        public static User GetUser(string userName)
        {
            lock (myUsers)
            {
                User aUser = myUsers.FirstOrDefault(x => x.UserName == userName);
                return aUser;
            }
        }
    }

    // Encapsulates the SRP authentication.
    internal static class SrpAuthentication
    {
        // Connection context for each connected client.
        private class ConnectionContext
        {
            public ConnectionContext(string responseReceiverId, string userName)
            {
                ResponseReceiverId = responseReceiverId;
                UserName = userName;
                ThreadId = -1;
            }

            // Identifies the connection session.
            public string ResponseReceiverId { get; private set; }

            // Identifies the thread which is right now handling the request.
            public int ThreadId { get; set; }

            // Login name.
            public string UserName { get; private set; }

            // SRP values used during the authentication process.
            public byte[] K { get; set; }
            public byte[] A { get; set; }
            public byte[] B { get; set; }
            public byte[] s { get; set; }

            // Serializer to serialize/deserialize messages once
            // the authenticated connection is established.
            // It uses the session key (calculated during RSP authentication)
            // to encrypt/decrypt messages. 
            public ISerializer Serializer { get; set; }
        }

        // List of connected clients.
        private static List<ConnectionContext> myConnections = new List<ConnectionContext>();

        // Returns SRP response for login request.
        public static object GetLoginResponseMessage(
            string responseReceiverId, object loginRequestMessage)
        {
            // Deserialize the login request.
            ISerializer aSerializer = new BinarySerializer();
            LoginRequestMessage aLoginRequest =
                aSerializer.Deserialize<LoginRequestMessage>(loginRequestMessage);

            // Try to find the user in database.
            UserDb.User aUser = UserDb.GetUser(aLoginRequest.UserName);
            if (aUser != null &&
                SRP.IsValid_A(aLoginRequest.A))
            {
                // Generate random service private ephemeral value.
                byte[] b = SRP.b();

                // Calculate service public ephemeral value.
                byte[] B = SRP.B(b, aUser.Verifier);

                // Calculate random scrambling value.
                byte[] u = SRP.u(aLoginRequest.A, B);

                // Calculate session key.
                byte[] K = SRP.K_Service(aLoginRequest.A, aUser.Verifier, u, b);

                // Prepare response message for the client.
                // Note: client is then supposed to calculate the session key
                //       and send back the message proving it was able to calculate
                //       the same session key.
                LoginResponseMessage aLoginResponse = new LoginResponseMessage();
                aLoginResponse.s = aUser.Salt; // user salt
                aLoginResponse.B = B;       // service public ephemeral value
                object aLoginResponseMessage =
                    aSerializer.Serialize<LoginResponseMessage>(aLoginResponse);

                // Store the connection context.
                ConnectionContext aConnection = new ConnectionContext(responseReceiverId, aUser.UserName);
                aConnection.A = aLoginRequest.A;
                aConnection.B = B;
                aConnection.K = K;
                aConnection.s = aUser.Salt;
                lock (myConnections)
                {
                    myConnections.Add(aConnection);
                }

                // Send the response to the client.
                return aLoginResponseMessage;
            }

            // The client will be disconnected.
            return null;
        }

        // Checks if the connecting client was able to calculate the same
        // session key as the service.
        // If the user entered an incorrect password the calculation
        // will be different.
        public static bool Authenticate(string responseReceiverId, object M1)
        {
            ConnectionContext aConnection;
            lock (myConnections)
            {
                aConnection = myConnections.FirstOrDefault(
                    x => x.ResponseReceiverId == responseReceiverId);
            }
            if (aConnection != null)
            {
                // Proving message from the client.
                byte[] aClientM1 = (byte[])M1;

                // Service calculates the proving message too.
                byte[] aServiceM1 = SRP.M1(aConnection.A, aConnection.B, aConnection.K);

                // If both messages are equql then it means the client proved its identity
                // and the connection can be established.
                if (aServiceM1.SequenceEqual(aClientM1))
                {
                    // Create serializer which will encrypt the communication.
                    Rfc2898DeriveBytes anRfc = new Rfc2898DeriveBytes(aConnection.K, aConnection.s, 1000);
                    ISerializer aSerializer = new AesSerializer(new BinarySerializer(), anRfc, 256);

                    // Store serializer which will encrypt using the calculated key.
                    aConnection.Serializer = aSerializer;

                    // Clean properties which are not needed anymore.
                    aConnection.A = null;
                    aConnection.B = null;
                    aConnection.K = null;
                    aConnection.s = null;

                    return true;
                }

                lock (myConnections)
                {
                    myConnections.RemoveAll(
                        x => x.ResponseReceiverId == responseReceiverId);
                }
            }

            return false;
        }

        // Cancel the authentication sequense.
        public static void CancelAuthentication(string responseReceiverId)
        {
            lock (myConnections)
            {
                myConnections.RemoveAll(x => x.ResponseReceiverId == responseReceiverId);
            }
        }

        // Removes the connection.
        public static void CloseConnection(string responseReceiverId)
        {
            lock (myConnections)
            {
                ConnectionContext aConnection = myConnections.FirstOrDefault(
                    x => x.ResponseReceiverId == responseReceiverId);
                if (aConnection != null)
                {
                    myConnections.Remove(aConnection);
                }
            }
        }

        // Returns user name for the specific response receiver id.
        public static string GetUserName(string responseReceiverId)
        {
            string aUserName = "";
            lock (myConnections)
            {
                ConnectionContext aConnection = myConnections.FirstOrDefault(
                    x => x.ResponseReceiverId == responseReceiverId);
                if (aConnection != null)
                {
                    aUserName = aConnection.UserName;
                }
            }

            return aUserName;
        }

        // Returns user name for the current thread.
        public static string GetUserName()
        {
            string aUserName = "";
            lock (myConnections)
            {
                ConnectionContext aConnection = myConnections.FirstOrDefault(
                    x => x.ThreadId == Thread.CurrentThread.ManagedThreadId);
                if (aConnection != null)
                {
                    aUserName = aConnection.UserName;
                }
            }

            return aUserName;
        }

        // This method is called when the RPC service needs to serialize or deserialize
        // request/response for the particular user and connection.
        public static ISerializer GetConnectionSpecificSerializer(string responseReceiverId)
        {
            ConnectionContext aUserContext;
            lock (myConnections)
            {
                aUserContext = myConnections.FirstOrDefault(
                    x => x.ResponseReceiverId == responseReceiverId);
            }
            if (aUserContext != null)
            {
                // Store also the thread id so that it is possible to identify a user
                // from within the RPC method.
                // Note: this is a workaround which will be resolved in a future Eneter release.
                aUserContext.ThreadId = Thread.CurrentThread.ManagedThreadId;

                // Return the connection specific serializer.
                return aUserContext.Serializer;
            }

            throw new InvalidOperationException("Failed to get serializer for the given connection.");
        }
    }

    class Program
    {
         static void Main(string[] args)
        {
            // Display Eneter errors and warnings on the console.
            //EneterTrace.TraceLog = Console.Out;

            // Simulate database of users.
            UserDb.CreateUser("Peter", "pwd123");
            UserDb.CreateUser("Frank", "pwd456");

            try
            {
                // Instantiate class which provides the service.
                ICalculator aCalculator = new Calculator();

                // Create RPC service.
                IRpcFactory aFactory = new RpcFactory()
                {
                    // Note: this allows to encrypt/decrypt messages for each client
                    // individualy based on calculated session key.
                    SerializerProvider = OnGetSerializer
                };
                IRpcService<ICalculator> aRpcService =
                    aFactory.CreateSingleInstanceService<ICalculator>(aCalculator);

                // Use TCP for the communication.
                IMessagingSystemFactory aUnderlyingMessaging =
                    new TcpMessagingSystemFactory(new EasyProtocolFormatter());
                
                // Use communication which is authenticated via SRP.
                IMessagingSystemFactory aMessaging =
                    new AuthenticatedMessagingFactory(aUnderlyingMessaging,
                    OnGetLoginResponseMessage,
                    OnAuthenticate,
                    OnAuthenticationCancelled);

                // Crete input channel and attach it to the receiver to start listening.
                IDuplexInputChannel aInputChannel =
                    aMessaging.CreateDuplexInputChannel("tcp://127.0.0.1:8033/");
                aInputChannel.ResponseReceiverConnected += OnClientConnected;
                aInputChannel.ResponseReceiverDisconnected += OnClientDisconnected;

                // Attach inout channel to the RPC service and start listning.
                aRpcService.AttachDuplexInputChannel(aInputChannel);

                Console.WriteLine("RPC service is running. Press ENTER to stop.");
                Console.ReadLine();

                // Detach input channel to stop the listening thread.
                aRpcService.DetachDuplexInputChannel();
            }
            catch (Exception err)
            {
                EneterTrace.Error("RPC Service failed.", err);
            }
        }

        private static object OnGetLoginResponseMessage(string channelId,
                string responseReceiverId, object loginRequestMessage)
        {
            return SrpAuthentication.GetLoginResponseMessage(responseReceiverId, loginRequestMessage);
        }

        private static bool OnAuthenticate(string channelId, string responseReceiverId,
                object login, object handshakeMessage, object M1)
        {
            return SrpAuthentication.Authenticate(responseReceiverId, M1);
        }

        private static void OnAuthenticationCancelled(
                string channelId, string responseReceiverId, object loginMessage)
        {
            SrpAuthentication.CancelAuthentication(responseReceiverId);
        }


        private static void OnClientConnected(object sender, ResponseReceiverEventArgs e)
        {
            string aUserName = SrpAuthentication.GetUserName(e.ResponseReceiverId);
            Console.WriteLine(aUserName + " is logged in.");
        }

        // Remove the connection context if the client disconnects once the connection
        // was established after the successful authentication.
        private static void OnClientDisconnected(object sender, ResponseReceiverEventArgs e)
        {
            string aUserName = SrpAuthentication.GetUserName(e.ResponseReceiverId);
            SrpAuthentication.CloseConnection(e.ResponseReceiverId);
            Console.WriteLine(aUserName + " is logged out.");
        }

        // It is called by MultiTypedReceiver whenever it sends or receive a message from a connected client.
        // It returns the serializer for the particular connection (which uses the agreed session key).
        private static ISerializer OnGetSerializer(string responseReceiverId)
        {
            return SrpAuthentication.GetConnectionSpecificSerializer(responseReceiverId);
        }
       
    }
}


Client Application

The client is a simple winform application which provides UI for login. Then when connecting the service it uses the SRP protocol to authenticate. If the authentication is successful it uses the calculated secret key to encrypt the communication. 
using Eneter.Messaging.DataProcessing.Serializing;
using Eneter.Messaging.Diagnostic;
using Eneter.Messaging.EndPoints.Rpc;
using Eneter.Messaging.MessagingSystems.Composites.AuthenticatedConnection;
using Eneter.Messaging.MessagingSystems.ConnectionProtocols;
using Eneter.Messaging.MessagingSystems.MessagingSystemBase;
using Eneter.Messaging.MessagingSystems.TcpMessagingSystem;
using Eneter.Messaging.Threading.Dispatching;
using Eneter.SecureRemotePassword;
using System;
using System.IO;
using System.Security.Cryptography;
using System.Windows.Forms;

namespace WindowsFormClient
{
    public partial class Form1 : Form
    {
        // Interface declaring the RPC service.
        public interface ICalculator
        {
            double Sum(double a, double b);
        }

        // Encapsulates the SRP authentication for client.
        private class SrpClientAuthentication
        {
            private LoginRequestMessage myLoginRequest;
            private byte[] myPrivateKey_a;
            private ISerializer mySerializer;

            // It is called by the AuthenticationMessaging to get the login message.
            public object GetLoginRequestMessage(string userName)
            {
                myPrivateKey_a = SRP.a();
                byte[] A = SRP.A(myPrivateKey_a);

                myLoginRequest = new LoginRequestMessage();
                myLoginRequest.UserName = userName;
                myLoginRequest.A = A;

                // Serializer to serialize LoginRequestMessage.
                ISerializer aSerializer = new BinarySerializer();
                object aSerializedLoginRequest = aSerializer.Serialize<LoginRequestMessage>(myLoginRequest);

                // Send the login request to start negotiation about the session key.
                return aSerializedLoginRequest;
            }

            // It is called by the AuthenticationMessaging to handle the LoginResponseMessage received
            // from the service.
            public object GetProveMessage(
                string userPassword, object loginResponseMessage)
            {
                // Deserialize LoginResponseMessage.
                ISerializer aSerializer = new BinarySerializer();
                LoginResponseMessage aLoginResponse =
                    aSerializer.Deserialize<LoginResponseMessage>(loginResponseMessage);

                // Calculate scrambling parameter.
                byte[] u = SRP.u(myLoginRequest.A, aLoginResponse.B);

                if (SRP.IsValid_B_u(aLoginResponse.B, u))
                {
                    // Calculate user private key.
                    byte[] x = SRP.x(userPassword, aLoginResponse.s);

                    // Calculate the session key which will be used for the encryption.
                    // Note: if everything is then this key will be the same as on the service side.
                    byte[] K = SRP.K_Client(aLoginResponse.B, x, u, myPrivateKey_a);

                    // Create serializer which will encrypt the communication.
                    Rfc2898DeriveBytes anRfc = new Rfc2898DeriveBytes(K, aLoginResponse.s, 1000);
                    mySerializer = new AesSerializer(new BinarySerializer(), anRfc, 256);

                    // Create M1 message to prove that the client has the correct session key.
                    byte[] M1 = SRP.M1(myLoginRequest.A, aLoginResponse.B, K);
                    return M1;
                }

                // Close the connection with the service.
                return null;
            }

            // It is called whenever the client sends or receives the message from the service.
            // It will return the serializer which serializes/deserializes messages using
            // the connection password.
            public ISerializer GetSerializer()
            {
                return mySerializer;
            }
        }

        private SrpClientAuthentication mySrpAuthentication = new SrpClientAuthentication();
        private IRpcClient<ICalculator> myRpcClient;
        
        

        public Form1()
        {
            InitializeComponent();
            EnableUiControls(false);
        }

        private void Form1_FormClosed(object sender, FormClosedEventArgs e)
        {
            CloseConnection();
        }

        private void OpenConnection()
        {
            IMessagingSystemFactory anUnderlyingMessaging =
                new TcpMessagingSystemFactory(new EasyProtocolFormatter());
            IMessagingSystemFactory aMessaging =
                new AuthenticatedMessagingFactory(anUnderlyingMessaging,
                                                  OnGetLoginRequestMessage,
                                                  OnGetProveMessage)
            {
                // Timeout for the authentication.
                // If the value is -1 then it is infinite (e.g. for debugging purposses)
                AuthenticationTimeout = TimeSpan.FromMilliseconds(30000)
            };

            IDuplexOutputChannel anOutputChannel =
                aMessaging.CreateDuplexOutputChannel("tcp://127.0.0.1:8033/");
            anOutputChannel.ConnectionClosed += OnConnectionClosed;

            IRpcFactory aFactory = new RpcFactory()
            {
                SerializerProvider = OnGetSerializer
            };
            myRpcClient = aFactory.CreateClient<ICalculator>();

            try
            {
                // Attach output channel and be able to call RPC.
                myRpcClient.AttachDuplexOutputChannel(anOutputChannel);
                EnableUiControls(true);
            }
            catch
            {
                MessageBox.Show("Incorrect user name or password.",
                    "Login Failure", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
        }

        // It is called if the service closes the connection.
        private void OnConnectionClosed(object sender, DuplexChannelEventArgs e)
        {
            // It does not have to be invoked in the main UI thread.
            // So in order to manipulate UI controls dispatch the call
            // to the main UI thread.
            if (InvokeRequired)
            {
                Action aCloseConnection = CloseConnection;
                Invoke(aCloseConnection, null);
            }
            else
            {
                CloseConnection();
            }
        }

        private void CloseConnection()
        {
            // Detach output channel and release the thread listening to responses.
            if (myRpcClient != null && myRpcClient.IsDuplexOutputChannelAttached)
            {
                myRpcClient.DetachDuplexOutputChannel();
            }
            EnableUiControls(false);
        }

        private void EnableUiControls(bool isLoggedIn)
        {
            LoginTextBox.Enabled = !isLoggedIn;
            PasswordTextBox.Enabled = !isLoggedIn;
            LoginBtn.Enabled = !isLoggedIn;

            LogoutBtn.Enabled = isLoggedIn;
            Number1TextBox.Enabled = isLoggedIn;
            Number2TextBox.Enabled = isLoggedIn;
            CalculateBtn.Enabled = isLoggedIn;
            ResultTextBox.Enabled = isLoggedIn;
        }

        // It is called by the AuthenticationMessaging to get the login message.
        private object OnGetLoginRequestMessage(string channelId, string responseReceiverId)
        {
            string aUserName = LoginTextBox.Text;
            return mySrpAuthentication.GetLoginRequestMessage(aUserName);
        }

        // It is called by the AuthenticationMessaging to handle the LoginResponseMessage received
        // from the service.
        private object OnGetProveMessage(
            string channelId, string responseReceiverId, object loginResponseMessage)
        {
            string aUserPassword = PasswordTextBox.Text;
            return mySrpAuthentication.GetProveMessage(aUserPassword, loginResponseMessage);
        }

        // It is called whenever the client sends or receives the message from the service.
        // It will return the serializer which serializes/deserializes messages using
        // the connection password.
        private ISerializer OnGetSerializer(string responseReceiverId)
        {
            return mySrpAuthentication.GetSerializer();
        }


        private void CalculateBtn_Click(object sender, EventArgs e)
        {
            // Create message.
            double a = double.Parse(Number1TextBox.Text);
            double b = double.Parse(Number2TextBox.Text);
            double aResult = myRpcClient.Proxy.Sum(a, b);

            ResultTextBox.Text = aResult.ToString();
        }

        private void LoginBtn_Click(object sender, EventArgs e)
        {
            OpenConnection();
        }

        private void LogoutBtn_Click(object sender, EventArgs e)
        {
            CloseConnection();
        }
    }
}

4 comments:

  1. Thanks for the great example. Is there any way to invoke a client-methode diretly from the server ?

    ReplyDelete
  2. hi ondrej . can you create a example for CreatePerClientInstanceService ? i search and not find example for that.

    ReplyDelete
    Replies
    1. Hi although there is no explicit example here is at least a hint how to do it:

      On the service site:
      // Factory method which will be used by the service to create
      // instances of the service for each client.
      Func aFactoryMethod = () => new Calculator();

      IRpcService aRpcService = aFactory.CreatePerClientInstanceService(aFactoryMethod);

      Delete
  3. Thanks for this source- It is well documented and examples are great. I am working on a native C# HomeKit library and I think I will use your work for the SRP authentication Apple requires. I will use source code but full credits where due. Only reason not to use nuget is not sure if your nuget is .NET Standard? I dont think so. I want to be able to compile to native Linux and Mac from C# instead of use NodeJS - https://github.com/ppumkin/HomeKit - Will be updated in coming days once I get Alpha version functional - Thanks again!

    ReplyDelete