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 source code for this example and open it in Visual Studio.
- Download Eneter Messaging Framework for .NET platforms or get the nuget package directly via Visual Studio.
- Download Eneter Secure Remote Password or get the nuget package directly via Visual Studio.
- Compile projects and run the service and then the client application.
- Login with the name Peter and use 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:
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 as shown in the sequence diagram.
The source code of the library is a part of this article and can be downloaded here.
Or if you use Visual Studio with nugets you can directly link it into your project from the nuget.org server.
The interprocess communication is ensured by Eneter Messaging Framework which also provides AuthenticatedMessagingFactory which allows to implement any custom authentication e.g. also 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.
The whole implementation is here:
(The code is a bit longer but I believe it is not difficult to understand.)
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; }
}
private static HashSet<User> myUsers = new HashSet<User>();
private class ConnectionContext
{
public ConnectionContext(string responseReceiverId, string userName)
{
ResponseReceiverId = responseReceiverId;
UserName = userName;
}
public string ResponseReceiverId { get; private set; }
public string UserName { get; private set; }
public byte[] K { get; set; }
public byte[] A { get; set; }
public byte[] B { get; set; }
public byte[] s { get; set; }
public ISerializer Serializer { get; set; }
}
private static List<ConnectionContext> myConnections =
new List<ConnectionContext>();
static void Main(string[] args)
{
CreateUser("Peter", "pwd123");
CreateUser("Frank", "pwd456");
try
{
IMultiTypedMessagesFactory aFactory = new MultiTypedMessagesFactory()
{
SerializerProvider = OnGetSerializer
};
IMultiTypedMessageReceiver aReceiver =
aFactory.CreateMultiTypedMessageReceiver();
aReceiver.RegisterRequestMessageReceiver<CalculateRequestMessage>(OnCalculateRequest);
aReceiver.RegisterRequestMessageReceiver<int>(OnFactorialRequest);
IMessagingSystemFactory anUnderlyingMessaging =
new TcpMessagingSystemFactory(new EasyProtocolFormatter());
IMessagingSystemFactory aMessaging =
new AuthenticatedMessagingFactory(anUnderlyingMessaging,
OnGetLoginResponseMessage, OnAuthenticate, OnAuthenticationCancelled);
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();
aReceiver.DetachDuplexInputChannel();
}
catch (Exception err)
{
EneterTrace.Error("Service failed.", err);
}
}
private static object OnGetLoginResponseMessage(string channelId,
string responseReceiverId, object loginRequestMessage)
{
ISerializer aSerializer = new BinarySerializer();
LoginRequestMessage aLoginRequest =
aSerializer.Deserialize<LoginRequestMessage>(loginRequestMessage);
User aUser = GetUser(aLoginRequest.UserName);
if (aUser != null &&
SRP.IsValid_A(aLoginRequest.A))
{
byte[] b = SRP.b();
byte[] B = SRP.B(b, aUser.Verifier);
byte[] u = SRP.u(aLoginRequest.A, B);
byte[] K = SRP.K_Service(aLoginRequest.A, aUser.Verifier, u, b);
LoginResponseMessage aLoginResponse = new LoginResponseMessage();
aLoginResponse.s = aUser.Salt;
aLoginResponse.B = B;
object aLoginResponseMessage =
aSerializer.Serialize<LoginResponseMessage>(aLoginResponse);
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);
}
return aLoginResponseMessage;
}
return null;
}
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)
{
byte[] aClientM1 = (byte[])M1;
byte[] aServiceM1 = SRP.M1(aConnection.A, aConnection.B, aConnection.K);
if (aServiceM1.SequenceEqual(aClientM1))
{
Rfc2898DeriveBytes anRfc =
new Rfc2898DeriveBytes(aConnection.K, aConnection.s, 1000);
ISerializer aSerializer =
new AesSerializer(new BinarySerializer(true), anRfc, 256);
aConnection.Serializer = aSerializer;
aConnection.A = null;
aConnection.B = null;
aConnection.K = null;
aConnection.s = null;
return true;
}
}
lock (myConnections)
{
myConnections.RemoveAll(x => x.ResponseReceiverId == responseReceiverId);
}
return false;
}
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.");
}
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.");
}
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.");
}
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);
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);
}
}
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);
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)
{
byte[] s = SRP.s();
byte[] x = SRP.x(password, s);
byte[] v = SRP.v(x);
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 user name (Peter) and the password (pwd123) the client connects the service following the SRP authentication sequence. If the authentication is correct the connection is established and the user can consume the service. If the authentication fails the connection is not established and a dialog box about failed authentication is displayed.
When a 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 very simple too:
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)
{
OutputChannelThreading = new WinFormsDispatching(this),
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();
mySender.RegisterResponseMessageReceiver<CalculateResponseMessage>(
OnCalculateResponseMessage);
mySender.RegisterResponseMessageReceiver<int>(
OnFactorialResponseMessage);
try
{
mySender.AttachDuplexOutputChannel(anOutputChannel);
EnableUiControls(true);
}
catch
{
MessageBox.Show("Incorrect user name or password.",
"Login Failure", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private void OnConnectionClosed(object sender, DuplexChannelEventArgs e)
{
CloseConnection();
}
private void CloseConnection()
{
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;
}
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;
ISerializer aSerializer = new BinarySerializer();
object aSerializedLoginRequest =
aSerializer.Serialize<LoginRequestMessage>(myLoginRequest);
return aSerializedLoginRequest;
}
private object OnGetProveMessage(
string channelId, string responseReceiverId, object loginResponseMessage)
{
ISerializer aSerializer = new BinarySerializer();
LoginResponseMessage aLoginResponse =
aSerializer.Deserialize<LoginResponseMessage>(loginResponseMessage);
byte[] u = SRP.u(myLoginRequest.A, aLoginResponse.B);
if (SRP.IsValid_B_u(aLoginResponse.B, u))
{
byte[] x = SRP.x(PasswordTextBox.Text, aLoginResponse.s);
byte[] K = SRP.K_Client(aLoginResponse.B, x, u, myPrivateKey_a);
Rfc2898DeriveBytes anRfc = new Rfc2898DeriveBytes(K, aLoginResponse.s, 1000);
mySerializer = new AesSerializer(new BinarySerializer(true), anRfc, 256);
byte[] M1 = SRP.M1(myLoginRequest.A, aLoginResponse.B, K);
return M1;
}
return null;
}
private ISerializer OnGetSerializer(string responseReceiverId)
{
return mySerializer;
}
private void CalculateBtn_Click(object sender, EventArgs e)
{
CalculateRequestMessage aRequest = new CalculateRequestMessage();
aRequest.Number1 = double.Parse(Number1TextBox.Text);
aRequest.Number2 = double.Parse(Number2TextBox.Text);
mySender.SendRequestMessage<CalculateRequestMessage>(aRequest);
}
private void CalculateFactorialBtn_Click(object sender, EventArgs e)
{
int aNumber = int.Parse(FactorialNumberTextBox.Text);
mySender.SendRequestMessage<int>(aNumber);
}
private void OnCalculateResponseMessage(object sender,
TypedResponseReceivedEventArgs<CalculateResponseMessage> e)
{
ResultTextBox.Text = e.ResponseMessage.Result.ToString();
}
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();
}
}
}