65.9K
CodeProject 正在变化。 阅读更多。
Home

使用安全远程密码协议进行通信

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2016年3月4日

CPOL

3分钟阅读

viewsIcon

18978

downloadIcon

693

简单示例,展示如何使用 SRP 协议实现安全认证通信。

引言

以下示例演示了如何使用安全远程密码协议 (SRP)保护进程间通信。 该示例实现了一个简单的客户端-服务通信,其中客户端需要在消费服务之前进行身份验证(用户名 + 密码)。 身份验证完成后,后续通信将使用在 SRP 序列期间计算的密钥进行加密,该密钥对于每个连接都是唯一的。

运行示例

  1. 下载此示例的源代码并在 Visual Studio 中打开它。
  2. 从 .NET 平台下载Eneter Messaging Framework或直接通过 Visual Studio 获取 nuget 包
  3. 下载Eneter Secure Remote Password或直接通过 Visual Studio 获取 nuget 包
  4. 编译项目并运行服务,然后运行客户端应用程序。
  5. 使用用户名Peter和密码pwd123登录。

安全远程密码

SRP 是由斯坦福大学的 Thomas Wu 创建的一种协议,用于基于用户名和密码进行安全身份验证。
该协议很强大,即容忍各种攻击,防止对系统任何部分或部分的攻击导致进一步的安全风险。
它不需要任何受信任的第三方(例如,证书颁发者或 PKI),这使得它非常方便使用。
有关技术细节,请参考SRP 首页详细的 SRP 论文协议摘要

下图显示了在此示例中如何实现 SRP 序列

1082676/SrpAuthenticationSequence.png

Eneter.SecureRemotePassword

Eneter.SecureRemotePassword 是一个轻量级库,它实现了 SRP 公式并公开了用于实现 SRP 身份验证的 API。 API 命名约定与 SRP 规范匹配,因此在使用特定的 SRP 步骤(如序列图所示)时应该直观易懂。
该库的源代码是本文的一部分,可以在此处下载。
或者,如果您使用带有 nuget 的 Visual Studio,您可以从 nuget.org 服务器直接将其链接到您的项目中

进程间通信由 Eneter Messaging Framework 确保,该框架还提供AuthenticatedMessagingFactory,它允许实现任何自定义身份验证,例如,也包括使用 SRP 协议的身份验证。

服务应用程序

该服务是一个简单的控制台应用程序,它公开用于计算数字的服务。 它使用 SRP 来验证每个已连接的客户端。 仅当客户端提供正确的用户名和密码时,才会建立连接。 然后,整个通信使用 AES 进行加密。

当客户端请求登录用户时,将调用 OnGetLoginResponseMessage 方法。 它在数据库中查找用户,生成服务的秘密和公共临时值,然后计算会话密钥。 然后,它返回包含服务公共临时值和用户随机盐的消息。

当客户端发送 M1 消息来证明它知道密码时,将调用 OnAuthenticate 方法。 服务计算其自己的 M1 并将其与接收到的 M1 进行比较。 如果相等,则认为客户端已通过身份验证。
完整的实现就在这里

(代码有点长,但我相信理解起来并不难。)

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;
            }
        }
    }
}

客户端应用程序

客户端是一个简单的 win-form 应用程序,它提供登录功能。 一旦用户输入用户名 (Peter) 和密码 (pwd123),客户端就会按照 SRP 身份验证序列连接服务。 如果身份验证正确,则建立连接,用户可以使用该服务。 如果身份验证失败,则不建立连接,并显示一个关于身份验证失败的对话框。

当用户按下登录按钮时,客户端将尝试使用 SRP 序列打开连接。 AuthenticatedMessaging 将调用 OnGetLoginRequestMessage 方法。 它生成客户端的私有和公共临时值,并返回包含用户名和公共客户端临时值的登录请求消息。

然后,当服务发送登录响应时,将调用 OnGetProveMessage 方法。 它计算会话密钥和 M1 消息以证明它知道密码。

客户端的实现也非常简单

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();
        }
    }
}
© . All rights reserved.