The source code for this example can be found here.
Introduction
The example bellow demonstrates how to secure the interprocess communication using the Secure Remote Password protocol (SRP). The example implements a simple client-service communication where the client needs to authenticate (user name + password) before consuming the service. Once the authentication is complete the further communication is encrypted with the strong key which was calculated during the SRP sequence and which is unique for each connection.
To Run Example
- Download this example and open it in Visual Studio.
- Download Eneter Messaging Framework for .NET platforms or get the nuget package.
- Download Eneter Secure Remote Password or get the nuget package.
- Compile projects and run the service and then the client application.
- Login with the name Peter and the password pwd123.
Secure Remote Password
SRP is a protocol which was created by Thomas Wu at Stanford University to allow the secure authentication based on a user name and a password.
The protocol is robust i.e. tolerates wide range of attacks, preventing an attack on any part or parts of the system from leading to further security compromises.
It does not require any trusted third party (e.g. a certificate issuer or PKI) which makes it very comfortable to use.
For technical details please refer to SRP home page or detailed SRP paper or protocol summary.
The following diagram shows how the SRP sequence is implemented in this example:
The interprocess communication is ensured by Eneter Messaging Framework which also provides AuthenticatedMessagingFactory which allows to implement any custom authentication e.g. the authentication using the SRP protocol.
The protocol is robust i.e. tolerates wide range of attacks, preventing an attack on any part or parts of the system from leading to further security compromises.
It does not require any trusted third party (e.g. a certificate issuer or PKI) which makes it very comfortable to use.
For technical details please refer to SRP home page or detailed SRP paper or protocol summary.
The following diagram shows how the SRP sequence is implemented in this example:
Eneter.SecureRemotePassword
Eneter.SecureRemotePassword is the lightweight library which implements SRP formulas and exposes API to implement the SRP authentication. The API naming convention matches with the SRP specification so it should be intuitive to use in particular SRP steps.
The interprocess communication is ensured by Eneter Messaging Framework which also provides AuthenticatedMessagingFactory which allows to implement any custom authentication e.g. the authentication using the SRP protocol.
Service Application
The service is a simple console application which exposes services to calculate numbers. It uses the SRP to authenticate each connected client. The connection is established only in case the client provides the correct user name and password. The entire communication is then encrypted using AES.
When a client requests to login a user the OnGetLoginResponseMessage method is called. It finds the user in the database and generates secret and public ephemeral values of the service and then calculates the session key. Then it returns the message which contains the service public ephemeral value and the user random salt.
When the client sends the M1 message to prove it knows the password the OnAuthenticate method is called. The service calculates its own M1 and compares it with the received one. If equal the client is considered authenticated.
When a client requests to login a user the OnGetLoginResponseMessage method is called. It finds the user in the database and generates secret and public ephemeral values of the service and then calculates the session key. Then it returns the message which contains the service public ephemeral value and the user random salt.
When the client sends the M1 message to prove it knows the password the OnAuthenticate method is called. The service calculates its own M1 and compares it with the received one. If equal the client is considered authenticated.
The whole implementation is very simple:
using Eneter.Messaging.DataProcessing.Serializing; using Eneter.Messaging.Diagnostic; using Eneter.Messaging.EndPoints.TypedMessages; 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; namespace Service { [Serializable] public class CalculateRequestMessage { public double Number1 { get; set; } public double Number2 { get; set; } } [Serializable] public class CalculateResponseMessage { public double Result { get; set; } } class Program { private 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; } } // Simulates user database. private static HashSet<User> myUsers = new HashSet<User>(); // Connection context for each connected client. private class ConnectionContext { public ConnectionContext(string responseReceiverId, string userName) { ResponseReceiverId = responseReceiverId; UserName = userName; } // Identifies the connection session. public string ResponseReceiverId { get; private 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>(); static void Main(string[] args) { // Simulate database of users. CreateUser("Peter", "pwd123"); CreateUser("Frank", "pwd456"); try { // Create multi-typed receiver. // Note: this receiver can receive multiple types of messages. IMultiTypedMessagesFactory aFactory = new MultiTypedMessagesFactory() { // Note: this allows to encrypt/decrypt messages // for each client individualy // based on calculated session key. SerializerProvider = OnGetSerializer }; IMultiTypedMessageReceiver aReceiver = aFactory.CreateMultiTypedMessageReceiver(); // Register types of messages which can be processed by the receiver. aReceiver.RegisterRequestMessageReceiver<CalculateRequestMessage>(OnCalculateRequest); aReceiver.RegisterRequestMessageReceiver<int>(OnFactorialRequest); // Use TCP for the communication. IMessagingSystemFactory anUnderlyingMessaging = new TcpMessagingSystemFactory(new EasyProtocolFormatter()); // Use authenticated communication. IMessagingSystemFactory aMessaging = new AuthenticatedMessagingFactory(anUnderlyingMessaging, OnGetLoginResponseMessage, OnAuthenticate, OnAuthenticationCancelled); // Crete input channel and attach it to the receiver to start listening. IDuplexInputChannel anInputChannel = aMessaging.CreateDuplexInputChannel("tcp://127.0.0.1:8033/"); anInputChannel.ResponseReceiverConnected += OnClientConnected; anInputChannel.ResponseReceiverDisconnected += OnClientDisconnected; aReceiver.AttachDuplexInputChannel(anInputChannel); Console.WriteLine("Service is running. Press ENTER to stop."); Console.ReadLine(); // Detach input channel to stop the listening thread. aReceiver.DetachDuplexInputChannel(); } catch (Exception err) { EneterTrace.Error("Service failed.", err); } } // It is called by AuthenticationMessaging to process the login request // from the client. private static object OnGetLoginResponseMessage(string channelId, 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. User aUser = 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; } // It is called by AuthenticationMessaging to process the message from the client // which shall prove the user provided the correct password and so the client was // able to calculate the same session key as the service. private static bool OnAuthenticate(string channelId, string responseReceiverId, object login, object handshakeMessage, 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. Rfc2898DeriveBytes anRfc = new Rfc2898DeriveBytes(aConnection.K, aConnection.s, 1000); ISerializer aSerializer = new AesSerializer(new BinarySerializer(true), 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; } // Remove the connection context if the client disconnects during // the authentication sequence. private static void OnAuthenticationCancelled( string channelId, string responseReceiverId, object loginMessage) { lock (myConnections) { myConnections.RemoveAll(x => x.ResponseReceiverId == responseReceiverId); } } private static void OnClientConnected(object sender, ResponseReceiverEventArgs e) { string aUserName = ""; lock (myConnections) { ConnectionContext aConnection = myConnections.FirstOrDefault( x => x.ResponseReceiverId == e.ResponseReceiverId); if (aConnection != null) { aUserName = aConnection.UserName; } } 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 = ""; lock (myConnections) { ConnectionContext aConnection = myConnections.FirstOrDefault( x => x.ResponseReceiverId == e.ResponseReceiverId); aUserName = aConnection.UserName; myConnections.Remove(aConnection); } 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) { ConnectionContext aUserContext; lock (myConnections) { aUserContext = myConnections.FirstOrDefault(x => x.ResponseReceiverId == responseReceiverId); } if (aUserContext != null) { return aUserContext.Serializer; } throw new InvalidOperationException("Failed to get serializer for the given connection."); } // It handles the request message from the client to calculate two numbers. private static void OnCalculateRequest( Object eventSender, TypedRequestReceivedEventArgs<CalculateRequestMessage> e) { ConnectionContext aUserContext; lock (myConnections) { aUserContext = myConnections.FirstOrDefault( x => x.ResponseReceiverId == e.ResponseReceiverId); } double aResult = e.RequestMessage.Number1 + e.RequestMessage.Number2; Console.WriteLine("User: " + aUserContext.UserName + " -> " + e.RequestMessage.Number1 + " + " + e.RequestMessage.Number2 + " = " + aResult); // Send back the result. IMultiTypedMessageReceiver aReceiver = (IMultiTypedMessageReceiver)eventSender; try { CalculateResponseMessage aResponse = new CalculateResponseMessage() { Result = aResult }; aReceiver.SendResponseMessage<CalculateResponseMessage>(e.ResponseReceiverId, aResponse); } catch (Exception err) { EneterTrace.Error("Failed to send the response message.", err); } } // It handles the request message from the client to calculate the factorial. private static void OnFactorialRequest(Object eventSender, TypedRequestReceivedEventArgs<int> e) { ConnectionContext aUserContext; lock (myConnections) { aUserContext = myConnections.FirstOrDefault(x => x.ResponseReceiverId == e.ResponseReceiverId); } int aResult = 1; for (int i = 1; i < e.RequestMessage; ++i) { aResult *= i; } Console.WriteLine("User: " + aUserContext.UserName + " -> " + e.RequestMessage + "! = " + aResult); // Send back the result. IMultiTypedMessageReceiver aReceiver = (IMultiTypedMessageReceiver)eventSender; try { aReceiver.SendResponseMessage<int>(e.ResponseReceiverId, aResult); } catch (Exception err) { EneterTrace.Error("Failed to send the response message.", err); } } private 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! User aUser = new User(userName, s, v); lock (myUsers) { myUsers.Add(aUser); } } private static User GetUser(string userName) { lock (myUsers) { User aUser = myUsers.FirstOrDefault(x => x.UserName == userName); return aUser; } } } }
Client Application
The client is a simple win-form application which provides the logging functionality. Once the user enters the username (Peter) and password (pwd123) the client connects the service and follows the SRP authentication sequence. If the authentication is correct the connection is established and the user can consume the service.
When the user presses the login button the client will try to open the connection using the SRP sequence. The AuthenticatedMessaging will call the OnGetLoginRequestMessage method. It generates private and public ephemeral values of the client and returns the login request message which contains the username and the public client ephemeral value.
Then when the service sends the response for the login the OnGetProveMessage method is called. It calculates the session key and the M1 message to prove it knows the password.
When the user presses the login button the client will try to open the connection using the SRP sequence. The AuthenticatedMessaging will call the OnGetLoginRequestMessage method. It generates private and public ephemeral values of the client and returns the login request message which contains the username and the public client ephemeral value.
Then when the service sends the response for the login the OnGetProveMessage method is called. It calculates the session key and the M1 message to prove it knows the password.
The implementation of the client is also very simple:
using Eneter.Messaging.DataProcessing.Serializing; using Eneter.Messaging.EndPoints.TypedMessages; 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.Security.Cryptography; using System.Windows.Forms; namespace WindowsFormClient { public partial class Form1 : Form { [Serializable] public class CalculateRequestMessage { public double Number1 { get; set; } public double Number2 { get; set; } } [Serializable] public class CalculateResponseMessage { public double Result { get; set; } } private IMultiTypedMessageSender mySender; private LoginRequestMessage myLoginRequest; private byte[] myPrivateKey_a; private ISerializer mySerializer; 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) { // Receive response messages in the main UI thread. // Note: UI controls can be accessed only from the UI thread. // So if this is not set then your message handling method would have to // route it manually. OutputChannelThreading = new WinFormsDispatching(this), // 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; IMultiTypedMessagesFactory aFactory = new MultiTypedMessagesFactory() { SerializerProvider = OnGetSerializer }; mySender = aFactory.CreateMultiTypedMessageSender(); // Register handlers for particular types of response messages. mySender.RegisterResponseMessageReceiver<CalculateResponseMessage>(OnCalculateResponseMessage); mySender.RegisterResponseMessageReceiver<int>(OnFactorialResponseMessage); try { // Attach output channel and be able to send messages and receive responses. mySender.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) { CloseConnection(); } private void CloseConnection() { // Detach output channel and release the thread listening to responses. if (mySender != null && mySender.IsDuplexOutputChannelAttached) { mySender.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; FactorialNumberTextBox.Enabled = isLoggedIn; CalculateFactorialBtn.Enabled = isLoggedIn; FactorialResultTextBox.Enabled = isLoggedIn; } // It is called by the AuthenticationMessaging to get the login message. private object OnGetLoginRequestMessage(string channelId, string responseReceiverId) { myPrivateKey_a = SRP.a(); byte[] A = SRP.A(myPrivateKey_a); myLoginRequest = new LoginRequestMessage(); myLoginRequest.UserName = LoginTextBox.Text; 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. private object OnGetProveMessage( string channelId, string responseReceiverId, 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(PasswordTextBox.Text, 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 for encrypting the communication. Rfc2898DeriveBytes anRfc = new Rfc2898DeriveBytes(K, aLoginResponse.s, 1000); mySerializer = new AesSerializer(new BinarySerializer(true), 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. private ISerializer OnGetSerializer(string responseReceiverId) { return mySerializer; } private void CalculateBtn_Click(object sender, EventArgs e) { // Create message. CalculateRequestMessage aRequest = new CalculateRequestMessage(); aRequest.Number1 = double.Parse(Number1TextBox.Text); aRequest.Number2 = double.Parse(Number2TextBox.Text); // Send message. mySender.SendRequestMessage<CalculateRequestMessage>(aRequest); } private void CalculateFactorialBtn_Click(object sender, EventArgs e) { // Create message. int aNumber = int.Parse(FactorialNumberTextBox.Text); // Send Message. mySender.SendRequestMessage<int>(aNumber); } // It is called when the service sents the response for calculation of two numbers. private void OnCalculateResponseMessage(object sender, TypedResponseReceivedEventArgs<CalculateResponseMessage> e) { ResultTextBox.Text = e.ResponseMessage.Result.ToString(); } // It is called when the service sents the response for the factorial calculation. private void OnFactorialResponseMessage(object sender, TypedResponseReceivedEventArgs<int> e) { FactorialResultTextBox.Text = e.ResponseMessage.ToString(); } private void LoginBtn_Click(object sender, EventArgs e) { OpenConnection(); } private void LogoutBtn_Click(object sender, EventArgs e) { CloseConnection(); } } }
I need your help,
ReplyDeleteI am trying to consume the Compact-RIO REST Service from my WinForm.
When i try this in browser, it is redirecting to silverlight page to authenticate(Using SRP Protocol). How to customize your sample code to connect the "Silverlight HTTP" supported one...
Thanks,
Anto,