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

加密 C# 和 PHP 之间的通信

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (34投票s)

2011年7月8日

GPL3

44分钟阅读

viewsIcon

217585

downloadIcon

4469

使用 AES 和 RSA 算法设置 C# 和 PHP 之间的安全加密通信。

CS to PHP Encryption

目录

引言

本文的第一部分提供了一些背景知识。第二部分介绍了如何安装并使示例脚本正常工作,以便您可以验证它们是否有效并以此为基础进行构建。第三部分讨论了如何将我编写的库用于您自己的应用程序,为您提供充分利用该库的指导。第四部分介绍了库的实际代码,其工作原理以及我做出这些选择的原因。

如果您和我一样,您可能会想了解更多关于我如何使 C# 和 PHP 在代码中协同工作的信息,而不仅仅是盲目使用我的库。但是,我包含了详细的使用说明,以防您只想复制、粘贴并使其工作。

背景

几周前,我正在用 C# 编写一个概念验证程序,该程序部分需要安全地连接到一个 PHP 脚本。我没有使用 SSL 的选项,所以我选择了更定制化的方案。我认为一种特定的加密算法无论用哪种语言实现,其工作方式都完全相同,因此我简单地使用了 C# 和 PHP 中提供的内置算法。大多数情况下,这个假设都是成立的;然而,在让 Microsoft 的库和 PHP 的 Mcrypt 库之间的加密实际工作时,我遇到了出奇的困难。经过几个小时的失败,我最终放弃了,并快速搜索了一个解决方案。通常,我几分钟就能找到接近我需求的一个示例,但我花了很多时间却找不到合适的解决方案。似乎没有人能够完全回答那些正在寻求帮助以完成我正在做的事情的许多人的问题。许多问题得到的回答不令人满意,有些没有回答整个主题,有些根本没有得到回应,偶尔有人会回复自己的问题,但只说“我搞定了”,却不通过发布他的解决方案来帮助我们其他人。我已经花了足够多的时间试图找到一个“快速”的解决方案,所以在这一点上,我继续自己编写。花了我整整一个周六,但我成功了。本文详细介绍了我是如何做到的,提供了一个您可以使用的库,并希望能帮助到那些和我一样陷入困境的人。

AES 算法

高级加密标准 (AES) 是一种广泛使用的分组密码,基于 Rinjdael 加密算法。说是基于,我的意思是 AES 是 Rinjdael 的一个子集,因为它具有固定的 128 位块大小,而 Rinjdael 支持 128、192 或 256 位的可变块长度。不要将块长度与密钥长度混淆。AES 支持 128、192 或 256 位的密钥长度(当您看到 AES-128、AES-192 或 AES-256 的引用时,数字表示密钥大小而不是块大小)。

AES 是一种对称算法,这意味着必须使用相同的密钥来加密和解密消息。虽然该算法很强大,但您必须有一种安全的方式将密钥同时传递给双方,而不会被任何人截获,否则他们就可以使用密钥读取您的消息。这就是我们需要非对称加密算法的地方。

RSA 算法

RSA 算法是一种非对称、公钥加密方法。这意味着涉及到两个不同的密钥:一个加密密钥和一个解密密钥。这些密钥分别称为公钥和私钥。私钥保存在服务器上,远离窥探,而公钥可以发送给任意数量的人——甚至是您的敌人。用一个密钥加密的任何内容都只能用另一个密钥解密——您甚至无法用加密消息的同一密钥来解密它。这样,服务器(例如 PHP 脚本)就可以发送一个 RSA 公钥给您,您可以使用该公钥加密一些只有服务器才能解密的内容。这就是您如何安全地将对称 AES 密钥传输到远程主机以供 AES 算法使用。

但既然您已经有了 RSA 密钥,为什么还要传输和使用 AES 密钥呢?因为 RSA 非常慢。实际上,它慢到我们只想用它来传输一个用于对称算法的密钥。这基本上就是您请求 HTTPS 网站时后台发生的事情。服务器将公钥发送给您,您使用公钥加密一个您生成的随机密钥,然后将该加密密钥发送给服务器以建立一个会话,通过该会话您可以通过 AES 加密消息安全地通信。这正是我的实现工作的方式。

请注意,在实际应用中,还有一个步骤是您的计算机通过验证证书颁发机构的签名来验证您从网站获得的公钥是否有效。没有这个步骤,一个决心要这样做的人就可以通过发行他自己制作的公钥来冒充您正在访问的网站。然后,这个攻击者就可以充当您正在访问的网站,半路截获您的消息,以明文阅读消息,然后用真实网站的公钥加密消息,再发送给真实网站。请注意,本文并未阻止这种攻击,因此您不应将此处使用的加密用于过于敏感的内容——对于此类应用程序,您应该购买一个签名证书用于 SSL 加密网站。

出于本文的目的,我们将使用自签名 RSA 证书。没有必要为这个应用程序购买证书,因为我们反正不会检查签名。我们也不会在我们的应用程序中收到浏览器警告,告诉我们颁发者不受信任。我们自己生成的密钥比我们实际需要的要安全得多。

基于混淆的加密

现在是时候警告您编写自己的加密算法的危险性了。虽然编写自己的加密方案可能很有趣(我以前也这样做过很多次),但用它来做任何事情都**不是**个好主意。我强调这一点似乎不够。许多人(包括我曾经)认为,通过创建自己的算法,攻击者将不知道算法和密钥,从而使破解难度加倍。

这种推理的缺陷是双重的。一是解密某物,您必须拥有算法。如果您拥有算法,即使它被混淆编译成汇编,它也可以被反编译和研究其中的缺陷。如果您的算法的强度,即使是一点点,依赖于别人不知道您的算法,那么它就不是一个安全的算法。算法的所有强度都应该在于所选密钥的强度。二是,如果您有一个被泄露的密钥,那么您只丢失了该密钥保护的信息,而使用不同密钥的任何人仍然是安全的。如果您有一个被泄露的算法,那么无论密钥如何加密,所有信息都可能被泄露。如果发现算法不好,那么您方案的 1024 位密钥可能一文不值。

“但是全世界都知道 AES 算法是如何工作的!算法很容易获得并向任何感兴趣的人解释,那么它怎么会比我自己的算法更安全呢?”简单地说,AES 和 RSA 已经经过了广泛的测试,并受到了专家的审查。世界上所有真正聪明的人花费大量时间试图破解它,但仍然失败了。他们通过他们的失败证明了其强度。始终使用已被证明是安全的算法,因为当您自己发明算法时,您不知道自己忽略了多少缺陷。

我认为 Garth J. Lancaster 的一条评论说得最好,他指出,尽管人们认为在安全问题上保持神秘是最好的方式,但他宁愿信任那些他可以验证的公开解决方案,而不是那些他不能验证的。他说(我绝对同意)在加密方案中唯一应该保密的是证书或密钥。[阅读评论]如果加密方法是已知的,它不应该削弱加密信息。事实上,如果攻击者知道您使用了 AES,他可能会从一开始就放弃尝试破解算法。您所要做的就是生成密码学上安全的密钥,并妥善保管它们。

然后是编写自己的算法的法律问题。我将省略细节,只说如果您的意图是使用您自己的加密方案来存储他人的敏感信息(如信用卡等),并且使用了有缺陷的方法,您可能会遇到很多麻烦。在处理他人的信息时,请务必使用批准的加密方案。查看 NIST.gov 上的 FIPS 140-2 标准,或者在此处查看所有文档。NIST.gov此处查看所有文档。FIPS 140-3 即将发布,请密切关注。如果您经营一家企业,则需要遵守这些标准。远离 DES,因为它已被证明,一个 56 位 DES 密钥可以在一天内被破解(一个 64 位 DES 密钥实际上只有 56 位,因为另外 8 位只是用于奇偶校验)。

撇开安全风险不谈,编写有价值的算法需要花费大量时间。为什么不直接调用您正在使用的语言中最有可能提供的内置加密函数呢?专家为您创建、测试和优化了它,所以只需将其放入并使用即可。

使示例正常工作

使用 OpenSSL 生成 RSA 密钥对

出于法律原因,OpenSSL 不包含在本文的下载中。您可以从此处手动下载并按照下面的说明进行安装。(可选地,您可以从 OpenSSL 的开发者那里获取,但那样您就需要自己编译源代码。)

从列表中下载最新的 Light 安装程序。对我来说,这是 v1.0.0d (32 位)。运行安装程序,如果您看到像下面这样的弹出窗口(请参见图片),请单击“确定”——我们只需要访问此包中的 3 个文件,所以不会发生任何坏事。

当您到达“将 OpenSSL DLL 复制到:”屏幕时(参见图片),选择“OpenSSL 二进制文件(*/bin)目录”选项。

现在 OpenSSL 已安装,请在资源管理器窗口中导航到您安装它的文件夹。我将其安装在默认位置 C:\OpenSSL-Win32\bin。进入该目录后,打开一个命令窗口并 CD 到该目录(例如,键入“cd C:\OpenSSL-Win32\bin”)。现在您可以生成公钥和私钥 RSA 密钥了。

要生成您自己的 RSA 密钥,请在提示符处键入以下命令。

让我们生成一个 1024 位 RSA 私钥并将其存储为 temp.key

openssl genrsa -aes256 -out temp.key 1024

您可以选择将上面的 1024 更改为 2048,如果您想要一个更安全的密钥(但这是不必要的——即使是网站也大多使用 1024 位密钥);只要知道它在加密和解密时会稍慢一些。现在,将私钥转换为我们将要使用的格式

openssl rsa -in temp.key -out private.key

在下一步中,OpenSSL 将要求您输入有关您的网站的一些信息。您可以随意填写。像这样创建公用证书(用于分发给客户端)

openssl req -new -x509 -nodes -sha1 -key private.key -out public.crt -days 3650

如果愿意,下载中包含一个我编写的 .bat 文件,您可以将其复制到 OpenSSL 的 bin 文件夹并运行它来为您生成 SSL 密钥对。它会暂停以询问您有关密码和国家信息等内容,因此只需提供它所需的信息,它就会为您生成一个 RSA 密钥对。

我制作的一个密钥对包含在下载文件中供您试用(以防您无法使其正常工作或只是想跳过此步骤)。请注意,由于该私钥可供下载,全世界都可以访问它,因此请不要将其用于任何重要事项。

现在您拥有了 RSA 加密所需的公钥和私钥。为了在服务器上保护我们的私钥,我们还需要进行最后一步。您会注意到私钥以纯文本格式保存在 .key 文件中。如果我们将它按原样上传到我们的网站,那么任何人都可以通过简单地访问 yoursit e.com/private.key 并下载它来查看它。您可以设置您的 Web 服务器不允许人们下载 .key 文件(这是一种更难的方法,也是我们不会这样做的方法),或者您可以将其存储在 PHP 脚本的变量中。如果您尝试打开 PHP 文件,您只会看到一个空白页面而不是密钥。要做到这一点,我们只需要将 private.key 的内容复制到一个名为 $PrivateRSAKey 的 PHP 变量中,如下所示

<?php $PrivateRSAKey = "-----BEGIN RSA PRIVATE KEY----- 
                        MIIEowIBAAKCAQE...0FgBdzxrcF0b
                        -----END RSA PRIVATE KEY-----"; ?>

现在我们可以使用 PHP include() 函数将私钥加载到我们的 PHP 脚本中。要使用我加密库中的函数,变量必须具有上面给出的名称($PrivateRSAKey),否则将无法识别。另外,请确保不要将 private.key 文件上传到您的 Web 服务器!这将是一个重大的安全错误。一旦您生成了一个好的 private.key 的 .php 文件,您最好删除 private.key,以免它到处乱飞。在下载中,我提供了一个简单的工具(由 5 行 C# 编写)来自动将 .key 文件转换为 .php 文件中的变量。如果您在复制和粘贴字符串时遇到问题,或者弄错了格式或换行符,您可以使用它。

现在您拥有了证书格式的公钥(public.crt)和存储在 PHP 变量中的私钥(private.php)。

在服务器端设置 PHP 脚本

下一步是将脚本的 PHP 部分上传到支持 PHP 的 Web 服务器。我将 PHP 脚本上传到了我本地网络上运行 WAMP(Windows、Apache、MySQL 和 PHP)的另一台计算机上。将本文下载的 PHP 文件夹中的所有内容上传到您想要安全访问的 Web 服务器上的一个文件夹中。我将其上传到了我另一台计算机的 Web 根目录下的一个名为“enc”的文件夹中,因此对我而言,脚本将位于 http://skot2/enc/。接下来,将(在上面的第 1 步中创建的)public.crt 和 private.php 文件放入此文件夹,覆盖包含的示例密钥。(如果您想暂时使用我的密钥,直到您弄好为止,请稍后再复制密钥。)请注意,在您将私钥上传到 Web 服务器的短短几秒钟内,它可能会被拦截。如果这是一个问题,那么您应该通过 HTTPS 链接上传它到 Web 服务器,或者找到一种在服务器本身上生成它的方法。这是私钥唯一一次跨网络传输。

本文使用的 PHP 加密库是 PHP Sec Lib。该库是我们将在其中使用的常用加密算法的优秀纯 PHP 实现。PHP Sec Lib 的代码包含在此下载文件中,供您方便使用,但也可以从SourceForge下载。

现在您已将所需文件上传到您的网站,并且应该在 Internet 上有一个正常工作的 PHP 脚本,您可以从连接的 C# 端进行访问。

在客户端设置 C# 库

就设置一个正常工作的示例而言,这是最容易的部分。只需在 Visual Studio 中打开下载中的解决方案文件,然后按编译。

运行示例

程序运行时,在第三个框中,输入您上传到网站的 example.php 的完整 URL(对我而言,它是 http://skot2/enc/example.php),然后单击“建立连接”。等待片刻。当它显示连接已建立(“发送消息”按钮将可用)时,输入一条消息并按“发送消息”。如果一切正常,您将收到来自 PHP 的有意义的响应(而不是乱码或崩溃)。

C# 代码示例负责从服务器检索公钥并使用它来发送它生成的对称密钥。如果您愿意,您还可以测试 HTTP 异步 POST 功能,如果您输入了 test.php 的正确 URL,或者通过输入 rsa.php 的正确 URL 来进行 RSA 加密(不传输 AES 密钥)。

该示例默认为 http://skot2/,因为这是我家庭网络测试计算机的名称。

使用代码 - 演练

使用 PHP 库

我将通过引导您创建一个简单的应用程序来展示如何使用此库:一个高分提交者!我们将用 C# 创建一个蹩脚的游戏(您只需要输入您想要的分数)。游戏会将用户的最高分发布到我们的安全 PHP 脚本,该脚本将返回他在所有玩家中的排名。我们将加密此传输,因为我们不希望输不起的人使用 HTTP 拦截软件来捕获发布的数据(分数),修改他们的分数,然后重新发送数据包,以便他们能够在作弊榜上感到温暖。

让我们开始吧。在编写我们自己的代码之前,我们将看一下示例。如果您已经上传了整个示例文件夹,那么您也应该上传了 example.php。顾名思义,这是我们将要构建的示例页面。其内容如下所示

<?php

// Set the location to the public and private keys
$PrivateKeyFile = "private.php";
$PublicKeyFile  = "public.crt";

include("secure.php");

if ($AESMessage != "")
{
   SendEncryptedResponse("Got: " . $AESMessage . ", GOOD!");
}

?>

正如您所见,我们只需设置私钥(PHP 格式)和公钥的位置,包含加密库,然后编写我们自己的代码来执行任何操作。以下是一些注意事项...

  • 您的脚本除了 SendEncryptedResponse() 函数输出的内容外,不能输出任何数据。
  • 您必须设置两个密钥文件变量($PrivateKeyFile 和 $PublicKeyFile),并且必须在 include 语句之前设置它们。$PrivateKeyFile 应该设置为包含您的私钥的 PHP 文件名(在上面第一部分生成),并且$PublicKeyFile 应该设置为公用证书文件的位置。
  • include 语句显然也是使用该库所必需的。
  • include() 语句和 ?> 结束标签之间的任何内容都是您的逻辑:您将如何处理安全传输的消息。

以下是如何处理传入的加密消息

每当此脚本收到有效的加密消息时,它将自动使用当前会话中的 AES 解密密钥进行解密(稍后您将从 C# 端建立此会话)。解密后,消息将存储在 $AESMessage 变量中。如果此变量不为空(因此检查 != ""),那么该变量中就有一个纯文本消息,您可以根据需要进行处理。在示例中,我只是将其连同一些额外内容一起发送回发送者。我们将对此进行一些修改,首先解析出获胜者的姓名和分数。由于我们最终将以 name(comma)score 的格式从 C# 端发送这些,让我们使用 PHP 的 explode(delimiter, string) 函数来解析这两个字符串

if ($AESMessage != "")
{
   // Get the username and high score from the message that was sent
   $split = explode(",", $AESMessage);
   $username = $split[0];
   $score = $split[1];
}

添加一些排名代码怎么样?

$rank = "";
if ($score < 100)
   $rank = "Loser!";
else if ($score < 1000)
   $rank = "Not bad...";
else if ($score < 10000)
   $rank = "Pretty Good.";
else if ($score < 100000)
   $rank = "Amazing!";
else if ($score < 1000000)
   $rank = "~YOU DA BOMB~";
else
   $rank = "YOU ARE A GRAND MASTER!";

现在我们已经完成了对发送给我们的数据所要做的处理,我们可以使用 SendEncryptedResponse() 函数向访问此页面的客户端发送一个加密的响应。此函数会自动加密您给它的文本并将其返回给客户端。请注意,您只能调用此函数一次,一旦调用,脚本就会退出。在此函数调用之后,将不再处理任何内容。作为提醒,请不要在脚本中的任何地方使用 echo 命令,因为这会弄乱发送给客户端的消息。这是我们响应的方式

SendEncryptedResponse("Name: " . $username . " Rank: " . $rank);

调用此函数后,您就完成了。您传递给它的消息将被加密并发送到最初调用此页面的 C# 程序。以下是 PHP 脚本的完整源代码

<?php

// Set the location to the public and private keys
$PrivateKeyFile = "private.php";
$PublicKeyFile  = "public.crt";
include("secure.php");

if ($AESMessage != "")
{
   // Get the username and high score from the message that was sent
   $split = explode(",", $AESMessage);
   $username = $split[0];
   $score = $split[1];

   $rank = "";
   if ($score < 100)
      $rank = "Loser!";
   else if ($score < 1000)
      $rank = "Not bad...";
   else if ($score < 10000)
      $rank = "Pretty Good.";
   else if ($score < 100000)
      $rank = "Amazing!";
   else if ($score < 1000000)
      $rank = "~YOU DA BOMB~";
   else
      $rank = "YOU ARE A GRAND MASTER!";
      
   SendEncryptedResponse("Name: " . $username . " Rank: " . $rank);
}

?>

PHP 部分到此结束。让我们继续 C# 库...

使用 C# 库

我们已经设置好了伪高分榜,现在只需要编写一个伪游戏来发送一些数据。首先,打开 Visual Studio 并创建一个新的 Windows Forms 应用程序。进去之后,将一个 Textbox、一个 NumericUpDown 和一个 Button 拖到窗体上。让它看起来漂亮一点,添加一些 Label,并确保将 NumericUpDown 的 Maximum 属性设置为一个非常大的值,比如一亿。这是我做的“游戏”界面的样子

现在为按钮添加一个 OnClick 事件(在 Designer 视图中双击按钮)。在 Solution Explorer 窗格中,右键单击 References 并单击“Add Reference”。浏览找到 cs2phpCryptography.dll 库并添加它(您可以在下载的 Library 文件夹中找到它)。还要确保在代码隐藏文件的顶部添加对该库的引用,如下所示

using CS2PHPCryptography;

希望您不会遇到困难。我尽量说得更清楚一些,以防阅读本文的读者主要是 PHP 开发人员,对 C# 不太熟悉。

接下来,我们将在 Form 类中创建一个 SecurePHPConnection 对象,并按如下方式实例化它

SecurePHPConnection secure;

public Form1()
{
    InitializeComponent();

    secure = new SecurePHPConnection();
}

在实例化语句的紧下方,我们将订阅该类的两个事件:OnConnectionEstablished 和 OnResponseReceived。OnConnectionEstablished 将在如我自描述的命名所示,与远程 PHP 脚本建立安全连接时触发。OnResponseReceived 将在每次收到来自远程脚本的响应时触发(这仅发生在发送消息之后)。这是您订阅的方式

public Form1()
{
    InitializeComponent();

    secure = new SecurePHPConnection();
    secure.OnConnectionEstablished += 
      new SecurePHPConnection.ConnectionEstablishedHandler(
      secure_OnConnectionEstablished);
    secure.OnResponseReceived += 
      new SecurePHPConnection.ResponseReceivedHandler(
      secure_OnResponseReceived);
}

void secure_OnResponseReceived(object sender, ResponseReceivedEventArgs e)
{
    throw new NotImplementedException();
}

void secure_OnConnectionEstablished(object sender, 
            OnConnectionEstablishedArgs e)
{
    throw new NotImplementedException();
}

我们快到了。现在我们需要为该类提供它将发布信息的 PHP 脚本的位置。以下是我们如何做到这一点(用您上传 PHP 脚本的位置替换我的 URL)

secure.SetRemotePhpScriptLocation("http://skot2/enc/score.php");

现在我们可以最终启动安全连接的请求了。它需要一段时间,因为我们正在使用 RSA(而且有些人可能会使用 2048 位密钥使其更慢),所以我将其设置为在后台进行所有操作,并在成功时通过上述事件进行回调。这是您启动调用的操作

secure.EstablishSecureConnectionAsync();

您可能还希望将按钮的 Enabled 属性设置为 false,以便用户在连接建立之前无法单击“发送”按钮。只需添加以下代码

button1.Enabled = false;

很好!程序运行时会启动连接。现在在 OnConnectionEstablished 的事件代码中,我们可以启用该按钮,让用户知道现在可以提交他的高分了。

void secure_OnConnectionEstablished(object sender, OnConnectionEstablishedArgs e)
{
    button1.Enabled = true;
}

接下来我们将添加发送实际高分到 PHP 脚本的代码。我们可以在按钮单击事件代码中添加此代码。首先,我们将获取用户名和分数,并将它们放入一个由逗号分隔的字符串中,就像我们设置 PHP 脚本预期的那样

private void button1_Click(object sender, EventArgs e)
{
    string name = textBox1.Text;
    decimal score = (int)numericUpDown1.Value;
    string message = name + "," + score.ToString();
}

现在我们可以发送消息了。在检查消息是否可以发送之后,我们只需调用 SendMessageSecureAsync() 函数,让我们的消息使用 256 位 AES 密钥自动加密并发送到 PHP 脚本。

if (secure.OKToSendMessage)
{
    secure.SendMessageSecureAsync(message);
}

一旦 PHP 处理了请求并发送了响应,我们就可以在 OnResponseReceived 事件中处理它的响应。传递到事件中的 e 变量包含来自远程服务器的响应。我选择使用 MessageBox 来显示响应

void secure_OnResponseReceived(object sender, ResponseReceivedEventArgs e)
{
    MessageBox.Show(e.Response);
}

现在是时候按 F5 进行编译和测试了!应用程序启动后,按钮将灰显一段时间,直到连接建立。一旦启用,输入姓名和分数,发送出去,看看会发生什么。

对我来说是有效的。希望对您也有效。这是 Lame Game 的完整 C# 代码列表

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using CS2PHPCryptography;

namespace Lame_Game
{
    public partial class Form1 : Form
    {
        SecurePHPConnection secure;

        public Form1()
        {
            InitializeComponent();

            secure = new SecurePHPConnection();
            secure.OnConnectionEstablished += 
              new SecurePHPConnection.ConnectionEstablishedHandler(
              secure_OnConnectionEstablished);
            secure.OnResponseReceived += 
              new SecurePHPConnection.ResponseReceivedHandler(
              secure_OnResponseReceived);

            secure.SetRemotePhpScriptLocation("http://skot2/enc/score.php");
            secure.EstablishSecureConnectionAsync();

            button1.Enabled = false;
        }

        void secure_OnResponseReceived(object sender, ResponseReceivedEventArgs e)
        {
            MessageBox.Show(e.Response);
        }

        void secure_OnConnectionEstablished(object sender, 
                    OnConnectionEstablishedArgs e)
        {
            button1.Enabled = true;
        }

        private void button1_Click(object sender, EventArgs e)
        {
            string name = textBox1.Text;
            decimal score = (int)numericUpDown1.Value;
            string message = name + "," + score.ToString();

            if (secure.OKToSendMessage)
            {
                secure.SendMessageSecureAsync(message);
            }
        }
    }
}

考虑到我们正在与远程服务器进行 AES 加密消息通信,这相当简单的代码。好的。现在进入酷炫的部分...

代码工作原理

实际在互联网上传输的是什么?

我整理了这个图表,以直观地展示互联网上实际发生的情况

首先,C# 程序(下称客户端)将纯文本请求“getkey=y”发布到 PHP 脚本(服务器)。

其次,服务器看到客户端想要 RSA 公钥,所以它以纯文本形式将其返回给网页请求的响应。

第三,客户端使用密码学上安全的随机数生成器生成一个 256 位 AES 密钥和一个 128 位初始化向量。密钥和初始化向量(IV)构成了我们需要的对称密钥,以便连接的两端都能正确地使用 AES 算法进行加密和解密。密钥和 IV 都使用 RSA 公钥(由服务器提供)单独加密,并放入 POST 请求的数据部分,然后发送到服务器。

第四,服务器使用其 RSA 私钥解密 AES 密钥和 IV,并将其存储在服务器上的会话变量中,专供此一个客户端使用。如果多个客户端连接到同一服务器,它们将拥有不同的 AES 密钥。服务器现在拥有所需的密钥,并使用它将字符串“AES OK”发送回客户端,该字符串已使用 AES 密钥进行了加密。

第五,客户端获取来自服务器的响应,使用为此会话建立的 AES 密钥解密它,然后验证它是否显示“AES OK”。如果不是,则表示存在问题。此时,已建立安全连接。每次进行初始化过程时,都会创建一个新的 AES 密钥,仅用于该会话,然后在使用完毕后丢弃。

最后一个“阶段”更像是一个循环。客户端向服务器发送任何消息,服务器处理请求并发送消息回来,然后客户端处理服务器的响应。在上图的图表中,黑色箭头表示通过互联网传输的纯文本,蓝色线条表示 RSA 加密的数据包,红色线条表示 AES 加密的消息。

当客户端完成后,他(可选地)将 AES 加密的消息“CLOSE CONNECTION”发送给服务器,服务器随后销毁包含 AES 密钥的会话变量并返回“DISCONNECTED”。

在 PHP 中实现

实现 RSA

遵循我设定的模式,我们将首先查看 PHP 代码。因为它比 C# 代码短得多,所以效果很好。由于完整的代码在这里打印太长,我将只展示适用于我当前讨论的代码段。

让我们从 RSA 开始。首先,我想让您知道,我编写的库不允许服务器使用 RSA 加密,也不允许客户端使用 RSA 解密——您只能在客户端加密,在服务器端解密(这仅适用于 RSA,不适用于 AES,AES 可以在双向工作)。这不是因为我做不到,而是因为对于此实现,我只使用 RSA 来传输对称密钥,因此不需要双向工作。另外,由于算法很慢,您没有理由希望 RSA 双向工作,除非您将其用于某种签名验证,而这目前超出了本文的范围。

首先,客户端会将变量“getkey=y”发布到服务器。当服务器看到这一点时,它将简单地输出整个公钥文件给客户端,然后退出。

//
// The remote user is requesting a public certificate.
//
if (isset($_POST['getkey']))
{
   echo file_get_contents($PublicKeyFile);
   exit;
}

由于我们使用的是 PHP Sec Lib 中纯 PHP 实现的 RSA,因此 RSA 中的加密实际上非常简单。我从一个代码示例开始,初始化用于解密的引擎

$rsa = new Crypt_RSA();
$rsa->setEncryptionMode(CRYPT_RSA_ENCRYPTION_PKCS1);
$rsa->loadKey($PrivateRSAKey);

不难。请注意,加密模式设置为 PKCS1 (v2.1),这仅仅是我们正在使用的加密标准。PKCS1 的替代方案是 OAEP(Optimal Asymmetric Encryption Padding),它可能更安全一些,但我们不打算在此应用程序中使用它。PHP 变量 $PrivateRSAKey 来自我们之前生成的 private.php 文件,并包含在上面。如果您没有阅读该部分,只需知道私钥文件已从 .key 文件完整地复制到 .php 文件中的变量中,以便人们无法通过直接访问其 URL 来下载它。私钥本身采用 PKCS1 未加密格式。未加密的意思是私钥本身未加密——它完全能够用于加密。

当客户端向我们发送他选择的用于此会话的 AES 密钥时,它将与服务器提供的公钥一起加密发送。现在是时候介绍我在客户端和服务器代码中使用的特殊 URL 安全 Base64 编码了。这是 PHP 中的样子

function Base64UrlDecode($x)
{
   return base64_decode(str_replace(array('_','-'), array('/','+'), $x));
}

function Base64UrlEncode($x)
{
   return str_replace(array('/','+'), array('_','-'), base64_encode($x));
}

它只是标准的 base64 编码,所有“/”字符都替换为“_”,所有“+”字符都替换为“-”。这是使 base64 编码字符串对 URL 友好的标准方法。我们不一定需要从服务器端这样做,但我选择对所有传输都这样做,以确保所有消息都使用相同的格式。

实现 AES

回到解密用公钥加密的 AES 密钥,让我们看看它是如何完成的

$_SESSION['key'] = Base64UrlEncode($rsa->decrypt(Base64UrlDecode($_POST['key'])));
$_SESSION['iv']  = Base64UrlEncode($rsa->decrypt(Base64UrlDecode($_POST['iv'])));

请注意,密钥和初始化向量是分开发送的,并且它们都经过 base64 编码。我们将帖子中的 base64 编码字符串解码为字节数组,用我们刚刚加载的私钥进行解密,将字节数组重新 base64 编码为字符串,然后将其存储在会话变量中以供以后使用。现在我们有了一个安全传输的 AES 密钥,可以在此会话的其余部分使用。RSA 解密部分运行缓慢(几秒钟),但幸运的是,这是我们唯一需要它的地方——AES 现在已经设置好了,而且它真的很快。

以下是实现 AES 部分的方法。再次,初始化 AES 引擎非常简单

$aes = new Crypt_AES(CRYPT_AES_MODE_CBC);

$aes->setKeyLength(256);
$aes->setKey(Base64UrlDecode($_SESSION['key']));
$aes->setIV(Base64UrlDecode($_SESSION['iv']));
$aes->enablePadding(); // This is PKCS7

正如您所见,我们将密钥长度设置为 256 位,这是 AES 允许的最强的。接下来,我们将密钥本身设置为之前存储在 $_SESSION['key'] 中的 AES 密钥,并将初始化向量设置为当前会话中存储的值。我们启用的填充是 PKCS7 填充。我们使用的 AES 要求我们将消息填充到块大小(固定的 128 位,即 16 字节)的倍数。有几种类型的填充格式可以使用,这也是有些人无法在其他语言中正确解密他们的 PHP 加密消息的原因。PKCS7 通过添加值等于添加字节数的字节来填充消息到正确的长度。示例如下

The string: T  H  I  S  I  S  A  T  E  S  T
In Hex:     54 48 49 53 49 53 41 54 45 53 54

由于消息不是 16 字节的倍数(它是 11 字节),我们必须添加 5 字节(16 - 11 = 5)来填充到该长度。使用 PKCS7 填充,这 5 个字节中的每个字节都将具有十六进制值 05,该值来自我们正在添加的字节数

54 48 49 53 49 53 41 54 45 53 54 05 05 05 05 05

然后可以将此填充的消息通过 AES 密码进行处理。不要将其与零填充(消息仅用零填充)混淆,也不要与其他许多填充方案混淆。我们选择使用这种方法来使其与 C# 一起工作。

您上面看到的 CBC 模式是设置 AES 块密码的模式为使用 Cipher Block Chaining。这就是使用我们设置的初始化向量。CBC 的基本思想是,每个块都用从消息中前一个块获得的一些信息进行加密。这意味着如果您在解密消息中的一个块时遇到任何问题,所有后续的块都将变得不可读。一个有趣的附注是,初始化向量不一定需要保密(像密钥那样);但是,为了额外的安全起见,将其妥善保管仍然是一个好主意。

接下来,要解密消息,我们这样做

$AESMessage = $aes->decrypt(Base64UrlDecode($_POST['data']));

再次注意我们是如何在运行密码之前首先从 base64 编码的字符串中获取字节的。这行代码就是 $AESMessage 变量被设置的地方,您可以使用它来访问客户端发送给您的任何内容。一旦服务器处理了该变量中的内容,它将使用 SendEncryptedResponse() 函数将响应发送回客户端。此函数以与上面初始化相同的方式初始化 AES 块密码,然后执行此操作

echo Base64UrlEncode($aes->encrypt($message));

打印出包含加密消息的 base64 编码字符串。这就是客户端将收到的对其加密的 HTTP 请求的响应消息。在此之后,我们调用 PHP exit 命令以防止服务器输出任何更多文本。如果输出的内容不是一个单一的加密消息,那么客户端将感到困惑并崩溃(或者至少会显示一个问题通知)。

服务器端尚未讨论的唯一方面是我们在收到客户端请求时销毁会话的地方。

if ($AESMessage == "CLOSE CONNECTION")
{
   echo Base64UrlEncode($aes->encrypt("DISCONNECTED"));

   $_SESSION['key'] = "llama";
   $_SESSION['iv']  = "llama";
   unset($_SESSION['key']);
   unset($_SESSION['iv']);
   session_destroy();
   exit;
}

更改变量内容,然后取消设置它们,然后销毁会话,这可能不是必需的,但这样做也没有坏处。llama 代表我的伪数据,而且我听说它们擅长销毁数据。就在我们杀死会话之前,我们将发送最后一条加密消息给客户端,告诉他我们收到了他要求“卷铺盖走人”的请求。以这种纯文本方式发送这些命令的缺点是,如果客户端想发送字符串“CLOSE CONNECTION”用于他正在做的与我们的协议无关的事情,它将关闭他的连接。

PHP 代码到此结束。现在我们可以深入研究客户端 C# 了。

在 C# 中实现

辅助类概述

客户端库中有相当多的方面与加密没有直接关系。我们将快速浏览一下,以便您了解以后调用函数时正在发生什么。

首先,我们将查看 HttpControl 类,该类负责将实际数据通过 Internet 发送到服务器。

public string Post(string url, string data, ProxySettings settings)
{
    try
    {
        byte[] buffer = Encoding.ASCII.GetBytes(data);
        HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);

        // Use a proxy
        if (settings.UseProxy)
        {
            IWebProxy proxy = request.Proxy;
            WebProxy myProxy = new WebProxy();

            Uri newUri = new Uri(settings.ProxyAddress);
            myProxy.Address = newUri;

            myProxy.Credentials = new NetworkCredential(
              settings.ProxyUsername, settings.ProxyPassword);
            request.Proxy = myProxy;
        }

        // Send request
        request.Method = "POST";
        request.ContentType = "application/x-www-form-urlencoded";
        request.ContentLength = buffer.Length;
        request.CookieContainer = cookies;
        Stream postData = request.GetRequestStream();
        postData.Write(buffer, 0, buffer.Length);
        postData.Close();

        // Get and return response
        HttpWebResponse response = (HttpWebResponse)request.GetResponse();
        Stream Answer = response.GetResponseStream();
        StreamReader answer = new StreamReader(Answer);
        return answer.ReadToEnd();
    }
    catch (Exception ex)
    {
        return "ERROR: " + ex.Message;
    }
}

这几乎是从 C# 向网站 POST 数据的标准方法。除了加密部分之外,此库还提供了此类,以防您将来需要使用它。请注意以下重要代码行

request.CookieContainer = cookies; 

这是我们确保发送回识别我们会话的 Cookie 信息的地方。没有这行代码,我们就无法让 PHP 将我们的密钥和 IV 保存在整个会话中,这意味着我们所做的每个请求都不能与我们其他的所有请求相关联,而我们必须在每次事务中重新发送密钥。当我们显式发送服务器给我们的 Cookie(包含我们连接的 SESSIONID)时,服务器可以在其连接的客户端列表中查找我们的密钥和 IV,并恢复我们的会话。

由于我经常使用代理,我添加了 POST 和 GET 函数的必要重载,以防您将来需要通过代理发送信息。忽略我捕获所有错误并将错误消息作为字符串返回的事实。偶尔,您将无法访问某个页面,并且在获得加密响应而不是 HTML 格式的错误时,这可能会破坏代码中的某些内容。

还有一个 AsyncHttpControl 类,其功能与 HttpControl 完全相同,只是所有 POST 或 GET 的调用都将在后台运行,并在收到响应时通过 OnHttpResponse 事件引发一个带有响应文本的事件。这在 UI 环境中很有用,因为您不希望 UI 在用户不知情的情况下冻结。

还有一个名为 PostPackageBuilder 的类,它使创建 POST 数据更加直接。您只需通过 AddVariable(varName, varValue) 方法向集合添加一个标识符和值对,然后将其发送到 HttpControl 类。如果您不想手动将变量连接成“x=1&y=2&z=llama”的形式,那么这个类将使事情工作得更顺畅。

在继续加密内容之前,我们要查看的最后一个辅助类是静态 Utility 类。该类目前仅包含用于以我们特殊的 URL 安全 Base64 格式进行编码和解码的函数(在上面的 PHP 代码中已介绍)。

实现 RSA

让我们开始吧。首先,我将展示 RSA 类的类图,然后我们将讨论它。

要使用我们将要用于 RSA 加密的类,我们需要两个 using 语句

using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;

我们生成的证书并将要加载的是 X509 证书,这意味着它包含公钥、证书的颁发对象名称以及证书的有效期等信息。就我们而言,我们将忽略除公钥之外的所有内容,因为那是我们目前所需要的。验证签名和到期日期超出了本文的范围。

此类包含一个函数,用于从字符串或文件加载证书。字符串格式的证书就是将整个证书文件内容加载到字符串中;我们可以使用此函数直接从服务器响应我们请求公钥的响应中加载证书。这是我们示例的 1024 位公钥 RSA 证书的样子

-----BEGIN CERTIFICATE-----
MIID2jCCA0OgAwIBAgIJAPEru6Ch9es0MA0GCSqGSIb3DQEBBQUAMIGlMQswCQYD
VQQGEwJVUzEQMA4GA1UECBMHRmxvcmlkYTESMBAGA1UEBxMJUGVuc2Fjb2xhMRsw
GQYDVQQKExJTY290dCBUZXN0IENvbXBhbnkxGTAXBgNVBAsTEFNlY3VyaXR5IFNl
Y3Rpb24xFjAUBgNVBAMTDVNjb3R0IENsYXl0b24xIDAeBgkqhkiG9w0BCQEWEXNz
bEBzcGFya2hpdHouY29tMB4XDTExMDcwNDEzMDczM1oXDTIxMDcwMTEzMDczNFow
gaUxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdGbG9yaWRhMRIwEAYDVQQHEwlQZW5z
YWNvbGExGzAZBgNVBAoTElNjb3R0IFRlc3QgQ29tcGFueTEZMBcGA1UECxMQU2Vj
dXJpdHkgU2VjdGlvbjEWMBQGA1UEAxMNU2NvdHQgQ2xheXRvbjEgMB4GCSqGSIb3
DQEJARYRc3NsQHNwYXJraGl0ei5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ
AoGBAKLEwtnhSD3sUMidycowAhupy59PMh8FYX6ebKy4NYqEiFONzrujkGtAZgmU
aCAQBEmGcfBUDVd4ew72Xjikq0WhBUju+wmrIcgnQcIMAXMkZ2gBV12SkvCzRrJf
5zqO0rC0x/tBli/46KGrzyYLl7K3QFx3MQPNvVO+w/b0coatAgMBAAGjggEOMIIB
CjAdBgNVHQ4EFgQU+6E6OauoEUohJOAgC8OXU3xaHn4wgdoGA1UdIwSB0jCBz4AU
+6E6OauoEUohJOAgC8OXU3xaHn6hgaukgagwgaUxCzAJBgNVBAYTAlVTMRAwDgYD
VQQIEwdGbG9yaWRhMRIwEAYDVQQHEwlQZW5zYWNvbGExGzAZBgNVBAoTElNjb3R0
IFRlc3QgQ29tcGFueTEZMBcGA1UECxMQU2VjdXJpdHkgU2VjdGlvbjEWMBQGA1UE
AxMNU2NvdHQgQ2xheXRvbjEgMB4GCSqGSIb3DQEJARYRc3NsQHNwYXJraGl0ei5j
b22CCQDxK7ugofXrNDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4GBAJ8l
RVFiLgfxiHsrPvhY+i05FYnDskit9QTnBv2ScM7rfK+EKfOswjxv9sGdGqKaTYE6
84XCmrwxCx42hNOSgMGDiZAlNoBJdJbF/bw2Qr5HUmZ8G3L3UlB1+qyM0+JkXMqk
VcoIR7Ia5AGZHe9/QAwD3nA9rf3diH2LWATtgWNB
-----END CERTIFICATE-----

正如您所见,它是一个 base64 编码的字符串,夹在头部和尾部之间。我们将要使用的 X509Certificate2 类的构造函数需要一个包含此证书的 base64 部分的字符串;因此,我们需要将其解析出来

key = key.Split(new string[] { "-----" }, StringSplitOptions.RemoveEmptyEntries)[1];
key.Replace("\n", "");

这将把证书分割成三个字符串的数组 ["BEGIN CERTIFICATE", "MIID2jCC...ATtgWNB", "END CERTIFICATE"],我们想要第二个元素:base64 编码的证书。我们还需要删除换行符,将 base64 字符串重新组合成一个长字符串,因为它在文件中以每行 64 个字符的形式存储。

正确读取证书可能是让一些人尝试自行解决问题时感到困惑的地方之一,因为 X509Certificate2 类的构造函数之一需要 XML 格式的证书文件,而您有一个 OpenSSL 生成的密钥,格式不同。您可能也不知道该向构造函数发送什么格式的字节数组。现在我们有了 base64 编码的证书字符串,我们可以将其解码为字节数组并将其发送到构造函数

return new X509Certificate2(Convert.FromBase64String(key));

如果在加载密钥的上述任何语句失败,我们将捕获一般的 Exception 并抛出 FormatException,因为导致此代码失败的唯一原因应该是证书的格式不符合预期(例如,顶部缺少“-----BEGIN CERTIFICATE-----”标签)。

现在我们有了一个公用证书,我们可以用它来加密东西(稍后我们将加密一个 AES 密钥)。以下是如何使用公钥进行加密

RSACryptoServiceProvider publicprovider = (RSACryptoServiceProvider)cert.PublicKey.Key;
return publicprovider.Encrypt(message, false);

请记住,用公钥加密的数据**只能**用私钥解密,并且您应该只用 RSA 加密小消息,因为 RSA 速度很慢。现在我们可以看看 AES 算法了。

实现 AES

看看 AEStoPHPCryptography 类的类图,然后我们将设置 AES 引擎。首先,我们将需要为以下命名空间添加一个 using 语句

using System.Security.Cryptography;

请记住上面网络模型中的信息,客户端负责生成加密所需的 AES 密钥和初始化向量。我们将首先这样做,如下所示

Key = new byte[256 / 8];
IV = new byte[128 / 8];

RNGCryptoServiceProvider random = new RNGCryptoServiceProvider();
random.GetBytes(Key);
random.GetBytes(IV);

RNGCryptoServiceProvider 类提供了一个相当安全的伪随机数生成器,我们将使用它来在每次启动与服务器的新连接时生成一个新的 AES 密钥。请记住,每个客户端将生成并使用不同的 AES 密钥,并且服务器将跟踪在服务器端使用哪个密钥与哪个客户端一起使用。请注意,Key 是一个 32 字节(256 位)长的字节数组,IV 是一个 16 字节(128 位)长的字节数组。我们用随机字节填充两个数组来用作我们的密钥。如果字节不是随机的(例如,您输入了密码),那么我们将严重降低系统的安全性。这就是为什么我们将自动生成密钥,使用密码学上安全的随机数生成器。

现在我们有了密钥,我们可以用它们来加密和解密(我们将把它们发送给服务器,在另一个类中,所以请稍等)。首先,我们将加密一些东西。让我们初始化 AES 引擎

RijndaelManaged aes = new RijndaelManaged();
aes.Padding = PaddingMode.PKCS7;

您可能会问,为什么我们使用 RinjdaelManaged 类而不是某个 AES 类。这是因为 AES 本质上是 Rinjdael,但密钥长度固定(有关此更多信息,请参阅本文顶部的介绍)。请再次注意,我们将填充设置为 PKCS7。您应该还记得 PHP 部分的含义(如果不是,您可以向上滚动查看)。然后我们将密码设置为使用 CBC,密钥大小为 256 位,然后将其与我们刚刚生成的密钥和 IV 关联。

aes.Mode = CipherMode.CBC;
aes.KeySize = 256;
aes.Key = Key;
aes.IV = IV;

这是实际发生魔鬼的地方。我们创建一个 ICryptoTransform 对象,它将为我们执行实际的加密操作

ICryptoTransform encryptor = aes.CreateEncryptor(aes.Key, aes.IV);

我们需要一个地方来存储加密数据,所以创建一个 MemoryStream

MemoryStream msEncrypt = new MemoryStream();

CryptoStream 是我们将写入消息的内容。它将使用指定的加密算法(AES)并将数据加密后写入我们提供的任何流,在本例中是内存流。

CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write);

我们有一个写入加密数据的流,我们有一个向其中写入明文并加密数据的流,现在我们只需要一个 StreamWriter 来实际允许我们写入加密流

StreamWriter swEncrypt = new StreamWriter(csEncrypt);

这是我们写入加密消息的流,进而将加密消息写入 MemoryStream 的地方

swEncrypt.Write(plainText);

释放我们正在使用的资源

swEncrypt.Close();
csEncrypt.Close();
aes.Clear();

最后,我们将加密数据以字节数组的形式返回到 MemoryStream 中。这是我们将要编码为 base64 字符串并发送到服务器的数据。

return msEncrypt.ToArray();

解密的工作方式基本相同,只是我们从 CryptoStream 创建了一个 Decryptor,而不是 Encryptor

ICryptoTransform decryptor = aes.CreateDecryptor(aes.Key, aes.IV);

并且我们从 CryptoStream 读取字节,而不是向其中写入

string plaintext = srDecrypt.ReadToEnd();

为方便起见,这是加密函数的完整代码

/// <summary>
/// Encrypt a message and get the encrypted message in a URL safe form of base64.
/// </summary>
/// <param name="plainText">The message to encrypt.</param>
public string Encrypt(string plainText)
{
    return Utility.ToUrlSafeBase64(Encrypt2(plainText));
}

/// <summary>
/// Encrypt a message using AES.
/// </summary>
/// <param name="plainText">The message to encrypt.</param>
private byte[] Encrypt2(string plainText)
{
    try
    {
        RijndaelManaged aes = new RijndaelManaged();
        aes.Padding = PaddingMode.PKCS7;
        aes.Mode = CipherMode.CBC;
        aes.KeySize = 256;
        aes.Key = Key;
        aes.IV = IV;
        
        ICryptoTransform encryptor = aes.CreateEncryptor(aes.Key, aes.IV);

        MemoryStream msEncrypt = new MemoryStream();
        CryptoStream csEncrypt = 
          new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write);
        StreamWriter swEncrypt = new StreamWriter(csEncrypt);

        swEncrypt.Write(plainText);

        swEncrypt.Close();
        csEncrypt.Close();
        aes.Clear();

        return msEncrypt.ToArray();
    } 
    catch (Exception ex)
    {
        throw new CryptographicException("Problem trying to encrypt.", ex);
    }
}

您应该能够在一个不使用我的库的应用程序中让这个特定的函数工作,只需将其复制粘贴进去,将 Utility.ToUrlSafeBase64 更改为 Convert.ToBase64String,并确保您在某处创建了一个 32 字节长的密钥数组和一个 16 字节长的 IV 数组。但我们*都*在使用我的库,对吗?

使 C# 和 PHP 之间的 AES 和 RSA 工作

最后,我们终于来到了 C# 客户端库的核心,这篇文章一直都在引导大家:SecurePHPConnection 类!这个类为您完成了一切。它将在后台线程中执行以下操作,并在建立安全连接后引发一个事件

  1. 向服务器请求公钥
  2. 生成 AES 密钥
  3. 使用公钥加密 AES 密钥
  4. 将加密的 AES 密钥发送到服务器
  5. 等待响应,然后引发安全连接已建立的事件

连接建立后,其余部分(如开头)对您来说都是抽象的。您只需调用一个函数即可发送消息,并通过阻塞直到收到响应或等待指示收到响应的事件被引发来等待响应。

这是我们在构造函数中设置的内容

http = new HttpControl();
rsa = new RSAtoPHPCryptography();
aes = new AEStoPHPCryptography();

background = new BackgroundWorker();
background.DoWork += new DoWorkEventHandler(background_DoWork);
background.RunWorkerCompleted += 
  new RunWorkerCompletedEventHandler(background_RunWorkerCompleted);

sender = new BackgroundWorker();
sender.DoWork += new DoWorkEventHandler(sender_DoWork);
sender.RunWorkerCompleted += 
  new RunWorkerCompletedEventHandler(sender_RunWorkerCompleted);

正如您所见,我们使用 HttpControl 进行通信,使用 RSA 类进行密钥交换,以及使用 AES 类进行常规通信(这三个类之前都已讨论过)。我们设置了两个后台工作程序来处理耗时的操作,而不会阻塞 UI。该类定义了任何人都可以订阅的两个事件

/// <summary>
/// Event raised when a secure connection
/// has been established with the remote PHP script.
/// </summary>
public event ConnectionEstablishedHandler OnConnectionEstablished;
public delegate void ConnectionEstablishedHandler(object sender, 
                     OnConnectionEstablishedEventArgs e);

/// <summary>
/// Event raised when an encrypted transmission
/// is received as a response to something you sent.
/// </summary>
public event ResponseReceivedHandler OnResponseReceived;
public delegate void ResponseReceivedHandler(object sender, 
                     ResponseReceivedEventArgs e);

第一个将在我们成功完成后台连接例程(获取 RSA 密钥、发送 AES 密钥并验证一切正常)时引发。第二个将在我们收到对先前发送的异步消息的响应时引发(仅当调用 SendMessageSecureAsync() 时才会引发此事件,而不是调用阻塞的 SendMessageSecure() 函数)。

我们需要保存服务器 PHP 脚本的 URL,我们将要联系它,所以我们可以编写一个简单的 set 函数,如下所示

public void SetRemotePhpScriptLocation(string phpScriptLocation)
{
    address = phpScriptLocation;
    ...
}

当客户端准备好启动安全连接时,它调用此函数在后台启动连接请求

public void EstablishSecureConnectionAsync()
{
    if (!background.IsBusy)
        background.RunWorkerAsync();
}

这是建立实际连接的代码。首先,我们将发送对公共证书的请求

string cert = http.Post(address, "getkey=y");

一旦我们收到证书(在 cert 中),我们就可以使用它来初始化我们的 RSA 密码

rsa.LoadCertificateFromString(cert);

接下来,我们告诉 AES 类生成一个安全的 AES 密钥和 IV

aes.GenerateRandomKeys();

并使用我们刚刚从服务器获得的公钥对其进行加密

string key = Utility.ToUrlSafeBase64(rsa.Encrypt(aes.EncryptionKey));
string iv = Utility.ToUrlSafeBase64(rsa.Encrypt(aes.EncryptionIV));

现在我们可以使用 HttpControl 将密钥和 IV 安全地发送到服务器,以供我们在安全会话中使用

string result = http.Post(address, "key=" + key + "&iv=" + iv);

如果到目前为止一切顺利,我们应该会收到服务器的加密响应“AES OK”,表明服务器和我们的两边都一切正常。安全隧道(某种意义上的)已经建立!然后我们可以通过引发 OnConnectionEstablished 事件来通知客户端

if (OnConnectionEstablished != null)
{
    OnConnectionEstablished(this, new OnConnectionEstablishedEventArgs());
}

一旦客户端知道一切都已成功设置以允许与服务器进行加密通信,他就可以使用 SendMessageSecure() 函数发送任意数量的消息。代码如下

string encrypted = aes.Encrypt(message);
string response = http.Post(address, "data=" + encrypted);
return aes.Decrypt(response);

正如您所见,消息被加密并作为常规 POST 请求发送到服务器。服务器的响应被解密并返回给调用函数的任何人。这会一直重复,直到客户端退出或发送断开连接请求。

就这样!如果您将 XML 文档与此库的 .dll 一起复制,您应该可以获得足够的文档来帮助您充分利用它提供的所有功能。

总结

希望本文能帮助那些要么在 C# 和 PHP 之间实现加密方面遇到问题,要么只是对任何与加密相关的内容感兴趣的人。无论哪种方式,如果您使用此库创建了任何非常酷的东西,请给我发条消息——我很想看看。

链接

许可证

本文下载中包含的所有代码均根据 GPL v3 发布。我的库允许您在 C# 中加密并在 PHP 中解密,以及在 PHP 中加密并在 C# 中解密,可以使用 RSA 或 AES。

PHP Sec Lib,包含在下载中,是 SourceForge 的一个开源项目。(请参阅上面的链接。)

历史

  • 2015年5月9日
  • 2011年10月8日
    • 修复了 SendMessageSecure() 函数
    • 修复了一些损坏的标题链接
    • 将 generate.bat 和 PhpSafeKey.exe 添加到下载包中
    • 添加了一个关于混淆加密的嘶吼部分
  • 2011年7月8日
    • 本文首次发布
© . All rights reserved.