WPF 安全信使
WPF 安全信使
引言
这是一个简单的 C# WPF 安全信使应用程序。该应用程序旨在演示使用 Microsoft .NET 4.5 进行安全通信的方法,以及 WPF 平台常用的 MVVM 编程模式。
背景
这个应用程序最初是我在 2010 年的一次面试中创建的演示。它最初是一个 Winforms 应用程序,我认为它可以进行一些改进。
因此,我决定使用 MVVM 和 Command 编程模式将其重写为 WPF 应用程序。
使用代码
解决方案包括三个项目:一个 WPF 宿主项目,一个共享类库,以及一个服务器类库。如下图所示:

WPF 宿主项目同时包含服务器和客户端组件。客户端组件运行在主线程或 UI 线程上,而服务器组件运行在后台线程上。
首先,我将解释 ViewModel 和 Command 类,因为它们定义了所使用的编程模式。下面是 MainViewModel.cs 类的代码:
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Windows;
using Secure.Messenger.Server;
using Secure.Messenger.Shared;
namespace Secure.Messenger.WpfHost
{
public class MainViewModel : INotifyPropertyChanged
{
public MainViewModel()
{
SendData = new Command(SendDataExecute, SendDataCanExecute);
ConnectToServer = new Command(ConnectToServerExecute, ConnectToServerCanExecute);
RemoteIPAddress = LocalIPAddress().ToString();
SendMessage = "Hello";
if (!DesignerProperties.GetIsInDesignMode(new DependencyObject()))
{
StartServerThread();
}
}
#region Networking
CryptoServer _server;
TcpClient _client;
NetworkStream _strm;
public AutoResetEvent _serverStarted = new AutoResetEvent(false);
Thread ServerThread;
Int32 _port = 9050;
private void StartServerThread()
{
try
{
ServerThread = new Thread(new ThreadStart(StartServer));
ServerThread.IsBackground = true;
ServerThread.Priority = ThreadPriority.Normal;
ServerThread.Start();
_serverStarted.WaitOne(); //Wait Here Until Server Has Started
StatusMessages.Add("Local Server Started Successfully at : " + _remoteIPAddress);
}
catch (Exception ex)
{
StatusMessages.Add("Problem Starting The Local Server : " + ex.Message);
return;
}
}
private void ConnectingToServer()
{
try
{
MessageData mes = new MessageData("Connection Message 123");
_client = new TcpClient(_remoteIPAddress, _port);
_strm = _client.GetStream();
if (!CryptoHelper.SendData(_strm, mes))
throw new Exception("Send data failed");
_strm.Close();
_client.Close();
StatusMessages.Add("Remote Server Connection Successfull to : " + _remoteIPAddress);
NotConnectedVisibility = Visibility.Hidden;
}
catch (Exception ex)
{
StatusMessages.Add("Problem Connecting to Remote Server at : " + _remoteIPAddress);
StatusMessages.Add("Message : " + ex.Message);
return;
}
}
private void SendTextData()
{
MessageData mes = new MessageData(SendMessage);
_client = new TcpClient(_remoteIPAddress.ToString(), _port);
_strm = _client.GetStream();
CryptoHelper.SendData(_strm, mes);
_strm.Close();
_client.Close();
SentMessages.Add("Sent to " + _remoteIPAddress.ToString() + " > " + SendMessage);
//SendMessage = string.Empty;
}
void StartServer()
{
_server = new CryptoServer(_serverStarted);
_server.ReceivedData += server_ReceivedData;
_server.StartServer(LocalIPAddress(), _port);
}
void server_ReceivedData(object sender, EventArgs<string> e)
{
Application.Current.Dispatcher.Invoke(new Action(() =>
{
ReceivedMessages.Add("Received from " + _remoteIPAddress + " > " + e.Value);
}));
}
private IPAddress LocalIPAddress()
{
if (!System.Net.NetworkInformation.NetworkInterface.GetIsNetworkAvailable())
{
return null;
}
IPHostEntry host = Dns.GetHostEntry(Dns.GetHostName());
return host
.AddressList
.FirstOrDefault(ip => ip.AddressFamily == AddressFamily.InterNetwork);
}
#endregion
#region Visibility
private Visibility _notConnectedVisibility = Visibility.Visible;
public Visibility NotConnectedVisibility
{
get
{
return _notConnectedVisibility;
}
set
{
_notConnectedVisibility = value;
RaisePropertyChanged("NotConnectedVisibility");
}
}
#endregion
#region Commands
public Command SendData { get; set; }
public Boolean SendDataCanExecute(Object parameter)
{
return true;
}
public void SendDataExecute(Object parameter)
{
SendTextData();
}
public Command ConnectToServer { get; set; }
public Boolean ConnectToServerCanExecute(Object parameter)
{
return true;
}
public void ConnectToServerExecute(Object parameter)
{
ConnectingToServer();
}
#endregion
#region TextData
private String _remoteIPAddress = String.Empty;
public String RemoteIPAddress
{
get
{
return _remoteIPAddress;
}
set
{
_remoteIPAddress = value;
RaisePropertyChanged("RemoteIPAddress");
}
}
private String _sendMessage = String.Empty;
public String SendMessage
{
get
{
return _sendMessage;
}
set
{
_sendMessage = value;
RaisePropertyChanged("SendMessage");
}
}
#endregion
#region Observable Collections
private ObservableCollection<String> _receivedMessages = new ObservableCollection<string>();
public ObservableCollection<String> ReceivedMessages
{
get
{
return _receivedMessages;
}
set
{
_receivedMessages = value;
RaisePropertyChanged("ReceivedMessages");
}
}
private ObservableCollection<String> _sentMessages = new ObservableCollection<string>();
public ObservableCollection<String> SentMessages
{
get
{
return _sentMessages;
}
set
{
_sentMessages = value;
RaisePropertyChanged("SentMessages");
}
}
private ObservableCollection<String> _statusMessages = new ObservableCollection<string>();
public ObservableCollection<String> StatusMessages
{
get
{
return _statusMessages;
}
set
{
_statusMessages = value;
RaisePropertyChanged("StatusMessages");
}
}
#endregion
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName.ToString()));
}
}
#endregion
}
}
该类继承自 INotifyPropertyChanged,该接口的成员用于更新视图。在这种情况下是 MainWindow .axml。
在这种情况下,INotifyPropertyChanged 成员的实现是:
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName.ToString()));
}
}
当 ViewModel 的属性之一更新时,会调用 RaisePropertyChanged 方法,如下所示:
private String _sendMessage = String.Empty;
public String SendMessage
{
get
{
return _sendMessage;
}
set
{
_sendMessage = value;
RaisePropertyChanged("SendMessage");
}
}
MainViewModel.cs 在 axml 代码中连接到 MainWindow.xaml,如下所示:
<Window x:Class="Secure.Messenger.WpfHost.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Secure.Messenger.WpfHost"
Title="Secure Messenger Demo" Height="730" Width="705"
ResizeMode="NoResize" Icon="Images/lock.ico">
<Window.DataContext>
<local:MainViewModel />
</Window.DataContext>
命名空间 Secure.Messenger.WpfHost
在 Window 标签中被声明为 local
,然后 Window.Datacontext
被声明为该命名空间中的一个名为 MainViewModel
的类。
然后,视图中的控件可以绑定到 ViewModel 的属性,如下所示:
<TextBox Grid.Column="1" Grid.Row="2" Width="150" FontWeight="Medium" HorizontalAlignment="Left" Height="25" VerticalAlignment="Top"
Text="{Binding RemoteIPAddress, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
文本框的 Text
属性绑定到 ViewModel 的 RemoteIPAddress
属性。
接下来要解释的是 Command.cs
类,如下所示:
using System;
using System.Windows.Input;
namespace Secure.Messenger.WpfHost
{
public class Command : ICommand
{
public delegate void CommandOnExecute(object parameter);
public delegate bool CommandOnCanExecute(object parameter);
private CommandOnExecute _execute;
private CommandOnCanExecute _canExecute;
public Command(CommandOnExecute onExecuteMethod, CommandOnCanExecute onCanExecuteMethod)
{
_execute = onExecuteMethod;
_canExecute = onCanExecuteMethod;
}
#region ICommand Members
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public bool CanExecute(object parameter)
{
return _canExecute.Invoke(parameter);
}
public void Execute(object parameter)
{
_execute.Invoke(parameter);
}
#endregion
}
}
这个类在 MainViewModel.cs
类中使用,作为将控件的 Command 路径连接到可执行代码的一种方式,而无需依赖代码隐藏文件。ICommand
接口的实现确保命令能够被执行和调用。
Commands 在 ViewModel 中声明如下:
public Command SendData { get; set; }
public Boolean SendDataCanExecute(Object parameter)
{
return true;
}
public void SendDataExecute(Object parameter)
{
SendTextData();
}
实例化如下:
SendData = new Command(SendDataExecute, SendDataCanExecute);
在 ViewModel 的构造函数中。在视图中,Commands 定义如下:
<Button Grid.Row="8" Grid.Column="1" Width="100" HorizontalAlignment="Right" Content="Send Message"
Margin="0,0,210,0" Command="{Binding SendData}"/>
Button 控件的 Command
属性绑定到 ViewModel 的 SendData
属性。
ViewModel 还包含执行网络操作的代码,其中一些方法被分配给 Secure.Messenger.Server
项目中的一个名为 CryptoHelper
的帮助类。稍后我将详细解释网络操作。
接下来,这是 MainWindow 启动时的屏幕截图:

应用程序会找到它正在运行的机器的 IP 地址。然后应用程序在该 IP 地址上启动本地服务器实例,并将“目标 IP 地址”文本框填充为此 IP 地址,从而将应用程序设置为本地回环模式。
==============
请注意:此应用程序仅在计算机的 IP 地址位于本地区域网络 (LAN) 的预期范围内时运行。它们是:
IP 范围:10.0.0.0 – 10.255.255.255 子网掩码:255.0.0.0
IP 范围:172.16.0.0 – 172.31.255.255 子网掩码:255.240.0.0
IP 范围:192.168.0.0 – 192.168.255.255 子网掩码:255.255.0.0
这是由于在服务器中使用 TCPListener
类而造成的限制,该类只能在此地址范围内使用。我稍后将解释原因。
因此,如果您的计算机连接到 LAN(我确定是这样),那么此应用程序应该可以毫无问题地启动,除了您的防火墙可能会要求您授予使用所需端口的权限。在这种情况下是 端口 9050
。
===============
应用程序启动后,服务器组件将在一个单独的线程上创建并启动,如下所示:
private void StartServerThread()
{
try
{
ServerThread = new Thread(new ThreadStart(StartServer));
ServerThread.IsBackground = true;
ServerThread.Priority = ThreadPriority.Normal;
ServerThread.Start();
_serverStarted.WaitOne(); //Wait Here Until Server Has Started
StatusMessages.Add("Local Server Started Successfully at : " + _remoteIPAddress);
}
catch (Exception ex)
{
StatusMessages.Add("Problem Starting The Local Server : " + ex.Message);
return;
}
}
此方法在 ViewModel 的构造函数中被调用,如下所示:
public MainViewModel()
{
SendData = new Command(SendDataExecute, SendDataCanExecute);
ConnectToServer = new Command(ConnectToServerExecute, ConnectToServerCanExecute);
RemoteIPAddress = LocalIPAddress().ToString();
SendMessage = "Hello";
if (!DesignerProperties.GetIsInDesignMode(new DependencyObject()))
{
StartServerThread();
}
}
通过一个测试来判断当前是否处于设计模式。这确保了服务器线程不会在设计模式下启动。这导致我在开发此应用程序时,我的 Visual Studio 版本崩溃了。
start server 方法如下所示:
void StartServer()
{
_server = new CryptoServer(_serverStarted);
_server.ReceivedData += server_ReceivedData;
_server.StartServer(LocalIPAddress(), _port);
}
private IPAddress LocalIPAddress()
{
if (!System.Net.NetworkInformation.NetworkInterface.GetIsNetworkAvailable())
{
return null;
}
IPHostEntry host = Dns.GetHostEntry(Dns.GetHostName());
return host
.AddressList
.FirstOrDefault(ip => ip.AddressFamily == AddressFamily.InterNetwork);
}
CryptoServer 类如下所示:
using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using Secure.Messenger.Shared;
namespace Secure.Messenger.Server
{
public class CryptoServer
{
public event EventHandler<EventArgs<String>> ReceivedData;
AutoResetEvent _serverStarted;
public CryptoServer(AutoResetEvent serverStarted)
{
_serverStarted = serverStarted;
}
public void StartServer(IPAddress localIPAddress, int localPort)
{
TcpListener listener = null;
try
{
listener = new TcpListener(localIPAddress, localPort);
listener.Start();
_serverStarted.Set(); // Signals the main thread to say local server thread has been started
}
catch (Exception ex)
{
ReceivedData.SafeInvoke(this, new EventArgs<string>("From Server : Network Error : " + ex.Message));
return;
}
NetworkStream strm = null;
while (!listener.Pending())
{
TcpClient client = listener.AcceptTcpClient();
strm = client.GetStream();
MessageData mes = CryptoHelper.ReceiveData(strm);
if (mes != null)
{
if (mes.MessageBody != "Connection Message 123")
{
ReceivedData.SafeInvoke(this, new EventArgs<string>(mes.MessageBody));
}
}
else
ReceivedData.SafeInvoke(this, new EventArgs<string>("From Server : Deserialisation Error"));
}
}
}
}
可以看到,一个 AutoResetEvent
被传递到服务器类中,然后用于通知主线程服务器已成功启动。
使用一个 while 循环来检测 TCPListener
中是否有数据到达,这可能看起来是糟糕的编程,但由于在使用 TCPListener
时没有可订阅的事件,这是唯一可用的技术。
数据通过线程安全的事件调用技术从服务器发送回来,如下所示。然后,这个事件在 ViewModel 类中被引发,如下所示:
void server_ReceivedData(object sender, EventArgs<string> e)
{
Application.Current.Dispatcher.Invoke(new Action(() =>
{
ReceivedMessages.Add("Received from " + _remoteIPAddress + " > " + e.Value);
}));
}
接收方法然后必须使用 Dispatcher.Invoke
来访问 ReceivedMessages 集合属性,因为它只能在 UI 线程上访问。Dispatcher.Invoke
实现了这个要求。
可以看到,在这个类中使用了两个扩展方法,它们如下所示:
namespace System
{
public class EventArgs<T> : EventArgs
{
public EventArgs(T value)
{
_value = value;
}
private T _value;
public T Value
{
get { return _value; }
}
}
public static class Extensions
{
public static void SafeInvoke<T>(this EventHandler<T> eventToRaise, object sender, T e) where T : EventArgs
{
EventHandler<T> handler = eventToRaise;
if (handler != null)
{
handler(sender, e);
}
}
}
}
SafeInvoke<T> 方法用于以线程安全的方式调用事件,而 EventArgs<T> 则提供了一个通用的类型化 EventArgs 类,用于将数据发送回主线程。
因此,一旦服务器启动并成功连接到本地客户端(回环配置)或应用程序的客户端组件的另一个实例,它看起来就像这样:

然后就可以安全地发送消息,UI 看起来就像这样:

我使用虚拟机测试了该应用程序,然后截屏证明该应用程序可以在 LAN 上使用。如下所示:

安全消息编程逻辑
安全消息是通过使用 TripleDESCryptoServiceProvider
类和 CryptoStream
类来实现的。
CryptoStream
类然后通过 MemoryStream
类在网络上传输。
用于发送和接收数据的代码如下所示:
public class CryptoHelper
{
public static Boolean SendData(NetworkStream strm, MessageData mes)
{
IFormatter formatter = new SoapFormatter();
MemoryStream memstrm = new MemoryStream();
TripleDESCryptoServiceProvider tdes = null;
CryptoStream csw = null;
try
{
byte[] Key = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16 };
byte[] IV = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16 };
tdes = new TripleDESCryptoServiceProvider();
csw = new CryptoStream(memstrm, tdes.CreateEncryptor(Key, IV), CryptoStreamMode.Write);
formatter.Serialize(csw, mes);
csw.FlushFinalBlock();
byte[] data = memstrm.GetBuffer();
int memsize = (int)memstrm.Length;
byte[] size = BitConverter.GetBytes(memsize);
strm.Write(size, 0, 4);
strm.Write(data, 0, (int)memsize);
return true;
}
catch (Exception)
{
return false;
}
finally
{
strm.Flush();
csw.Close();
memstrm.Close();
}
}
public static MessageData ReceiveData(NetworkStream strm)
{
MemoryStream memstrm = new MemoryStream();
byte[] Key = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16};
byte[] IV = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16};
TripleDESCryptoServiceProvider tdes = new TripleDESCryptoServiceProvider();
CryptoStream csw = new CryptoStream(memstrm, tdes.CreateDecryptor(Key, IV),
CryptoStreamMode.Write);
byte[] data = new byte[2048];
int recv = strm.Read(data, 0, 4);
int size = BitConverter.ToInt32(data, 0);
int offset = 0;
while (size > 0)
{
recv = strm.Read(data, 0, size);
csw.Write(data, offset, recv);
offset += recv;
size -= recv;
}
csw.FlushFinalBlock();
IFormatter formatter = new SoapFormatter();
memstrm.Position = 0;
MessageData mes = new MessageData(string.Empty);
try
{
mes = (MessageData)formatter.Deserialize(memstrm);
}
catch (SerializationException ex)
{
return null;
}
memstrm.Close();
return mes;
}
}
编程逻辑非常底层,我可以更详细地解释加密机制,但这篇文章已经很长了,我相信你们可以通过调试应用程序自己弄清楚。
可以看到,使用的加密密钥只是测试密钥,如果此应用程序被逆向工程(所有 .NET 程序集都可以被逆向工程),密钥就会被找到。因此,为了使此应用程序更安全,密钥必须存储在 Windows 注册表的 Current User Hive 中。然后,注册表项需要在安装时以某种方式添加。
最后
我相信你们中的一些人可能会问为什么这个应用程序不使用 WCF 架构,你们说得对。它应该使用 WCF 消息或传输安全,但我仍然认为这是一个 WPF 和直接加密技术结合的良好示例。
我计划编写这个应用程序的第二个版本,仍然使用 WPF,但结合 WCF 架构和一个监督客户端服务器元素(用于用户管理),所以请继续关注这个版本。
另外,这也是我使用简单的 TCPListener
类的一个原因,因为我只将其视为一个演示版本。
值得关注的点
在开发此应用程序时,我确实发现我不得不使用 Parallel Watch 窗口来捕获服务器线程中的一些断点。
历史
版本 1.0