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

Arduino (ESP32) 与 C# (ASP.NET) 之间的 AES 加密数据传输

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2023年7月30日

CPOL

6分钟阅读

viewsIcon

16375

downloadIcon

312

通过使用 AES 加密的 HTTP GET 请求在 Arduino 和 C# (ASP.NET) 之间进行通信。

图片:ESP32 模块

Arduino 和 C# 都有可用的 AES 加密库。因此,它们之间的数据传输可以进行消息加密。

我以前有过 AES 加密经验,我写了一篇关于 C# 中 AES 256 位实现的 文章。以下是该文章的链接:

然而,这是我第一次在 Arduino 项目中使用 AES。如果我的理解是正确的,AES 算法的基本原理在两种编程语言(C/C++ 和 C#)中实现方式相同。因此,用 C/C++ 加密并在 C# 中解密,反之亦然,用 C# 加密并在 C/C++ 中解密,应该没有问题。

好的,让我们开始吧

有五个元素(参数)会影响 AES 的工作方式。让我们识别它们:

  • 密码模式
  • 块/密钥大小(128 位、192 位、256 位)
  • 密钥字节
  • IV 字节(初始向量)
  • 填充模式

通过在两端(C# 和 C/C++)正确设置,它们应该产生相同的结果。

首先,密码模式

反编译 C# 程序集,得到以下结果:

namespace System.Security.Cryptography
{
    [Serializable]
    [ComVisible(true)]
    public enum CipherMode
    {
        CBC = 1,
        ECB,
        OFB,
        CFB,
        CTS
    }
}

CBC 是 C# AES 中使用的默认密码模式。

好的,让我们探索 Arduino 中的 AES 密码模式。

经过一番搜索,我找到并安装了 Matej Sychra 提供的名为 AESLib 的库。

Arduino 参考页面:https://www.arduino.cc/reference/en/libraries/aeslib/

作者的 Github:https://github.com/suculent/thinx-aes-lib

密钥/块大小和密码模式

根据描述,它使用 128 位 的密钥大小和 CBC 密码模式

密钥和 IV

由于 AESLib 使用 128 位密钥大小,这意味着 KeyIV 将由 16 个字节组成。16 个字节等于 128 位。每个字节可以表示 0 到 255 范围内的任何数字。

随机 16 字节,好的,这很容易。例如:

C# (ASP.NET)

byte[] aesKey = { 48, 85, 92, 36, 73, 111, 127, 18, 64, 38, 54, 89, 72, 105, 78, 15 };
byte[] aesIv = { 145, 33, 51, 67, 24, 173, 52, 209, 63, 42, 13, 115, 220, 3, 8, 10 };

C/C++ (Arduino)

byte aesKey[] = { 48, 85, 92, 36, 73, 111, 127, 18, 64, 38, 54, 89, 72, 105, 78, 15 };
byte aesIv[] = { 145, 33, 51, 67, 24, 173, 52, 209, 63, 42, 13, 115, 220, 3, 8, 10 };

KEYIV 有什么作用?

  • KEY:它是用于打乱(加密)和解密数据的秘密密码。
  • 初始向量 (IV):这通常是一个随机数,用于确保相同的数据在两次加密时看起来不同。然而,在我们的例子中,我们将硬编码 IV,因此相同的数据在每次加密时看起来都相同

在加密过程中,原始字节(或文本)将被分成多个块。KEYIV 都会影响每个块的打乱结果。

在解密(反转/解密)块时,必须使用相同的 KEYIV 来显示原始上下文,否则你会得到一堆乱七八糟的外星语言!

当一个块完全打乱后,一个新的 IV (一个向量)将被生成,通过遵循来自前一个数据块的特定模式来影响下一个块的打乱。每个块将始终使用相同的 KEY

KEY 通常被称为 Password,通常由最终用户(使用软件的人)提供。没有人,包括您(开发人员),会知道 KEY 数据是什么,它可以反转打乱。(除非您将用户的“密码”保存在数据库中)。

至于 IV,它通常在软件中硬编码。只有您(开发人员)会知道 IV 数据是什么。(当然,除非源代码泄露或有人逆向工程或反编译软件二进制代码)。

让我们用这个类比:KEY 就像第一个密码(公开的),而 IV第二个密码(私有的)。

然而,在这个 Arduino 项目中,两个“密码”(KEYIV)都将被硬编码并保密。

填充模式

我们将检查的最后一个元素是“填充模式”。

在 C# 中,可用的填充模式如下:

namespace System.Security.Cryptography
{
    [Serializable]
    [ComVisible(true)]
    public enum PaddingMode
    {
        None = 1,
        PKCS7,
        Zeros,
        ANSIX923,
        ISO10126
    }
}

PKCS7 是 C# AES 默认使用的填充模式。

然而,在 Arduino AESLib 中,通过探索作者提供的源代码(在此处找到:https://github.com/suculent/thinx-aes-lib/blob/master/src/AES.h),可用的填充模式列出如下(见第 41 行):

enum class paddingMode {
CMS,
Bit,
ZeroLength,
Null,
Space,
Random,
Array
};

有趣的是,这些似乎与 C# 中可用的不直接匹配。

然后,我继续探索另一个 C++ 源文件:https://github.com/suculent/thinx-aes-lib/blob/master/src/AES.cpp

在第 442 行:

...

* CMS (Cryptographic Message Syntax).
*     This pads with the same value as the number of padding bytes.
*     Defined in RFC 5652, PKCS#5, PKCS#7 (X.509 certificate) and RFC 1423 PEM.

...

PKCS#7 关键字在 CMS 填充模式的描述中提及,这与 C# 中的 PKCS7 填充模式一致。因此,我们将选择 CMS 填充模式在 Arduino C/C++ 中实现。

到此,所有参数都已确定。

  • 密码模式:CBC
  • 块/密钥大小:128 位
  • 密钥:16 字节
  • IV:16 字节
  • 填充模式:C# 中为 PKCS7,Arduino C/C++ 中为 CMS

让我们开始编码

我使用了一个名为 base64_encode 的库,由 dojyorin 提供,用于执行 base64 编码。

(什么是 base64 编码?https://www.youtube.com/watch?v=8qkxeZmKmOY

让我们从 Arduino C/C++ 中的加密开始。代码解释在代码行中的注释中提供:

// import AES encryption library
#include "AESLib.h"

// import base64 conversion library
#include "arduino_base64.hpp"

// declare a global AESLib object
AESLib aesLib;

// the text encryption function
String encrypt(String inputText) {

    // calculate the length of bytes of the input text
    // an extra of byte must be added for a null character
    // a null character will be filled as a text terminator
    // so that the process will not overflow to other parts of memory    
    int bytesInputLength = inputText.length() + 1;

    // declare an empty byte array (a memory storage)
    byte bytesInput[bytesInputLength];

    // convert the text into bytes, a null char is filled at the end
    inputText.getBytes(bytesInput, bytesInputLength);

    // calculate the length of bytes after encryption done
    int outputLength = aesLib.get_cipher_length(bytesInputLength);

    // declare an empty byte array (a memory storage)
    byte bytesEncrypted[outputLength];

    // initializing AES engine

    // Cipher Mode and Key Size are preset in AESLib
    // Cipher Mode = CBC
    // Key Size = 128

    // declare the KEY and IV
    byte aesKey[] = { 23, 45, 56, 67, 67, 87, 98, 12, 32, 34, 45, 56, 67, 87, 65, 5 };
    byte aesIv[] = { 123, 43, 46, 89, 29, 187, 58, 213, 78, 50, 19, 106, 205, 1, 5, 7 };

    // set the padding mode to paddingMode.CMS
    aesLib.set_paddingmode((paddingMode)0);

    // encrypt the bytes in "bytesInput" and store the output at "bytesEncrypted"
    // param 1 = the source bytes to be encrypted
    // param 2 = the length of source bytes
    // param 3 = the destination of encrypted bytes that will be saved
    // param 4 = KEY
    // param 5 = the length of KEY bytes (16)
    // param 6 = IV
    aesLib.encrypt(bytesInput, bytesInputLength, bytesEncrypted, aesKey, 16, aesIv);

    // declare a empty char array
    char base64EncodedOutput[base64::encodeLength(outputLength)];

    // convert the encrypted bytes into base64 string "base64EncodedOutput"
    base64::encode(bytesEncrypted, outputLength, base64EncodedOutput);

    // convert the encoded base64 char array into string
    return String(base64EncodedOutput);
}

C# 版本的加密

using System.Security.Cryptography;

static byte[] aesKey = { 23, 45, 56, 67, 67, 87, 98, 12, 32, 34, 45, 56, 67, 87, 65, 5 };
static byte[] aesIv = { 123, 43, 46, 89, 29, 187, 58, 213, 78, 50, 19, 106, 205, 1, 5, 7 };

public static string AesEncrypt(string originalText)
{
    // add a null character at the end
    // this is required by Arduino C/C++ as a string terminator
    // prevent Arduino process to overflow to other memory's data
    originalText += "\0";

    // convert the string into bytes (byte array)
    byte[] data = System.Text.Encoding.UTF8.GetBytes(originalText);

    // initialize AES encryption
    using (Aes aes = Aes.Create())
    {
        // set the AES parameters
        aes.KeySize = 128;
        aes.BlockSize = 128;
        aes.Mode = CipherMode.CBC;
        aes.Padding = PaddingMode.PKCS7;
        aes.Key = aesKey;
        aes.IV = aesIv;

        // Create an encryptor to encrypt the data
        ICryptoTransform encryptor = aes.CreateEncryptor(aes.Key, aes.IV);

        // create a memory stream for AES to store the encrypted bytes
        using (MemoryStream msEncrypt = new MemoryStream())
        {
            using (CryptoStream csEncrypt = 
                   new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
            {
                // begin the encryption process
                csEncrypt.Write(data, 0, data.Length);
                csEncrypt.FlushFinalBlock();

                // get the encrypted bytes
                data = msEncrypt.ToArray();
            }
        }
    }

    // convert the encrypted bytes into base64 string
    // sending this text to Arduino
    return Convert.ToBase64String(data);
}

接下来是解密,首先是 Arduino 版本

// the decryption function
String decrypt(String encryptedBase64Text) {

    // calculate the original length before it was coded into base64 string
    int originalBytesLength = base64::decodeLength(encryptedBase64Text.c_str());

    // declare empty byte array (a memory storage)
    byte encryptedBytes[originalBytesLength];
    byte decryptedBytes[originalBytesLength];

    // convert the base64 string into original bytes
    // which is the encryptedBytes
    base64::decode(encryptedBase64Text.c_str(), encryptedBytes);

    // initializing AES engine

    // Cipher Mode and Key Size are preset in AESLib
    // Cipher Mode = CBC
    // Key Size = 128

    // declare the KEY and IV
    byte aesKey[] = { 23, 45, 56, 67, 67, 87, 98, 12, 32, 34, 45, 56, 67, 87, 65, 5 };
    byte aesIv[] = { 123, 43, 46, 89, 29, 187, 58, 213, 78, 50, 19, 106, 205, 1, 5, 7 };

    // set the padding mode to paddingMode.CMS
    aesLib.set_paddingmode((paddingMode)0);

    // decrypt bytes in "encryptedBytes" and save the output in "decryptedBytes"
    // param 1 = the source bytes to be decrypted
    // param 2 = the length of source bytes
    // param 3 = the destination of decrypted bytes that will be saved
    // param 4 = KEY
    // param 5 = the length of KEY bytes (16)
    // param 6 = IV
    aesLib.decrypt(encryptedBytes, originalBytesLength, 
                   decryptedBytes, aesKey, 16, aesIv);

    // convert the decrypted bytes into original string
    String decryptedText = String((char*)decryptedBytes);

    return decryptedText;
}

C# 版本的解密

public static string AesDecrypt(string base64str)
{
    byte[] data = null;

    // the base64 string into bytes that's encrypted at Arduino
    byte[] encryptedData = Convert.FromBase64String(base64str);

    // initialize AES encryption
    using (Aes aes = Aes.Create())
    {
        // set the AES parameters
        aes.KeySize = 128;
        aes.BlockSize = 128;
        aes.Mode = CipherMode.CBC;
        aes.Padding = PaddingMode.PKCS7;
        aes.Key = aesKey;
        aes.IV = aesIv;

        // Create a decryptor to decrypt the data
        ICryptoTransform decryptor = aes.CreateDecryptor(aes.Key, aes.IV);

        // initialize memory stream to read data from the encrypted bytes
        using (MemoryStream msDecrypt = new MemoryStream(encryptedData))
        {
            // initialize the AES decryption engine
            using (CryptoStream csDecrypt = 
              new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
            {
                // declare a memory stream for AES to save the decrypted data
                using (MemoryStream originalMemoryStream = new MemoryStream())
                {
                    byte[] buffer = new byte[1024];
                    int readBytes;
                    while ((readBytes = csDecrypt.Read(buffer, 0, buffer.Length)) > 0)
                    {
                        originalMemoryStream.Write(buffer, 0, readBytes);
                    }

                    // extract the decrypted data from the memory stream
                    data = originalMemoryStream.ToArray();
                }
            }
        }
    }

    // Convert the bytes into string
    string text = System.Text.Encoding.UTF8.GetString(data);

    // remove the last null character (added by Arduino as line terminator)
    text = text.Remove(text.Length - 1, 1);

    return text;
}

构建应用程序

首先,ASP.NET 网站

现在,让我们构建 ASP.NET WebForms 应用程序来处理从 Arduino 发送的请求。

在 ASP.NET 解决方案资源管理器中,添加两个页面:

aes-decrypt.aspx
aes-encrypt.aspx 

(你可以随意命名。)

如果您愿意,可以路由页面。通过在项目中添加 Global.asax 并像这样路由页面:

protected void Application_Start(object sender, EventArgs e)
{
    RouteTable.Routes.MapPageRoute("aes-encrypt", "aes-encrypt", "~/aes-encrypt.aspx");
    RouteTable.Routes.MapPageRoute("aes-decrypt", "aes-decrypt", "~/aes-decrypt.aspx");
}

因此,这些页面可以像这样访问:

http://192.168.1.100:8080/aes-encrypt
http://192.168.1.100:8080/aes-decrypt

我写了另一篇文章解释 ASP.NET WebForms 中的路由。请随时通过此链接查看:

上面显示的端口 8080 是我在演示项目中使用的一个自定义端口,并且该站点由 Windows IIS Web 服务器运行。由于该页面只处理 HTTP GET 请求,您可以轻松地将代码迁移到 ASP.NET Core,它也可以托管在 Linux 上。

当您打开两个页面(aes-encrypt.aspxaes-decrypt.aspx)的前端时,您会发现以下代码:

<%@ Page Language="C#" AutoEventWireup="true" 
    CodeBehind="WebForm1.aspx.cs" Inherits="System.WebForm1" %>

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
</head>
<body>
    <form id="form1" runat="server">
        <div>
        </div>
    </form>
</body>
</html>

删除所有内容,除了第一行:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="WebForm1.aspx.cs"
   Inherits="System.WebForm1" %>

现在,编辑页面 aes-encrypt.aspx 的代码后台。这将模拟向 Arduino 发送加密消息的操作。

protected void Page_Load(object sender, EventArgs e)
{
    try
    {
        // this page will be initiated by GET request from Arduino

        string originalText = "mirror mirror on the wall who is the fairest of them all";

        string base64str = aes.AesEncrypt(originalText);

        // send out the encrypted string to Arduino
        Response.Write(base64str);
    }
    catch(Exception ex)
    {
        Response.Write("Error: " + ex.Message);
    }
}

接下来,编辑另一个页面 aes-decrypt.aspx。这将模拟接收 Arduino 发送的加密数据的操作:

protected void Page_Load(object sender, EventArgs e)
{
    try
    {
        // obtaining the GET request, the data appended after the symbol "?"
        // which is also called Query String
        string base64str = Request.Url.Query;

        // remove the 1st character "?"
        base64str = base64str.Remove(0, 1);

        // decode the URL characters of "%2B", "%2F", "%3D" into "+", "/", "="
        base64str = Server.UrlDecode(base64str);

        string text = aes.AesDecrypt(base64str);

        // send out the decrypted text to Arduino
        Response.Write(text);
    }
    catch (Exception ex) 
    {
        Response.Write("Error: " + ex.Message);
    }
}

Web 服务器端已完成。

编写 Arduino 代码

接下来,继续 Arduino 的编码。这里,我使用的是内置 WiFi 的 ESP32 模块。

// Provide HTTP Get request features
#include <WiFi.h>
#include <HTTPClient.h>

// The AES Encryption Library
#include "AESLib.h"
#include "arduino_base64.hpp"

// declare a global AESLib object
AESLib aesLib;

void setup() {
    
}

setup() 函数中,继续以下内容:

连接 WiFi

// begin the bit per second communication speed
// between Arduino and computer for serial port monitoring
Serial.begin(115200);

String encryptedText = "";
String decryptedText = "";
String url = "";

String wifi_ssid = "your_ssid";
String wifi_password = "your_ssid_pwd";

// begin connecting WiFi
WiFi.begin(wifi_ssid, wifi_password);

// check for WiFi connectivity status
while (WiFi.status() != WL_CONNECTED) {

    Serial.println("Connecting WiFi...");

    // wait for 1 second before re-checking WiFi status
    delay(1000);
}

Serial.println("WiFi connected!");

测试 1:本地 Arduino 加密和解密

Serial.println();
Serial.println("** Round 1: Local Test - Arduino Encrypt >> Arduino Decrypt");
Serial.println();

String text1 = "Luke, I am your father";
String text2 = encrypt(text1);
String text3 = decrypt(text2);

// send the output to the computer for monitoring & debugging
Serial.println("Original text: \"" + text1 + "\"");
Serial.println("Encrypted text: \"" + text2 + "\"");
Serial.println("Decrypted text: \"" + text3 + "\"");

测试 2:HTTP GET 请求 - Arduino 加密 >> C# 解密

Serial.println();
Serial.println("** Round 2: HTTP Get Request - Arduino Encrypt >> C# Decrypt");
Serial.println();

String originalText = "What if I told you everything you know to be true is wrong";

Serial.println("Original Text: \"" + originalText + "\"");
Serial.println("Begin arduino encryption process...");

encryptedText = encrypt(originalText);

Serial.println("Arduino encrypted text: \"" + encryptedText + "\"");

Serial.println("Sending encrypted text to server...");

// declare a http client
HTTPClient http;

// test server decryption

// encode the query data for URL
encryptedText.replace("+", "%2B");
encryptedText.replace("/", "%2F");
encryptedText.replace("=", "%3D");

// send the encrypted data to the ASP.NET Web Server
url = "http://192.168.1.100:8080/aes-decrypt?" + encryptedText;
Serial.println("URL: " + url);

http.begin(url);

// send a HTTP GET request
int httpResponseCode = http.GET();

// the ASP.NET web server received and responded to the message
if (httpResponseCode > 0) {
    Serial.print("HTTP success: ");
    Serial.println(httpResponseCode);

    decryptedText = http.getString(); 
    Serial.println("Returned server decrypted Text: \"" + decryptedText + "\"");

} else {
    Serial.print("Error occurred while sending HTTP request. Error code: ");
    Serial.println(httpResponseCode);
}

http.end();

测试 3:HTTP GET 请求 - C# 加密 >> Arduino 解密

Serial.println();
Serial.println("** Round 3: HTTP Get Request - C# Encrypt >> Arduino Decrypt");
Serial.println();

Serial.println("Downloading encrypted text from server...");

// get encrypted data from ASP.NET Web Server
url = "http://192.168.1.100:8080/aes-encrypt";
Serial.println("URL: " + url);

http.begin(url);

httpResponseCode = http.GET();

bool round2DownloadSuccess = false;

// encrypted data downloaded successfully
if (httpResponseCode > 0) {

    Serial.print("HTTP success: ");
    Serial.println(httpResponseCode);

    round2DownloadSuccess = true;

    encryptedText = http.getString();
    Serial.println("Received server encrypted text: \"" + encryptedText + "\"");

} else {
    Serial.print("Error occurred while sending HTTP request. Error code: ");
    Serial.println(httpResponseCode);
}

http.end();  // Close connection

if (round2DownloadSuccess) {
    Serial.println("Begin arduino decrypting process...");
    decryptedText = decrypt(encryptedText);
    Serial.println("Arduino decrypted Text: \"" + decryptedText + "\"");
}

完成。

如果您成功构建并设置了 ASP.NET Web 服务器,并运行 Arduino 代码,您应该会看到类似以下的输出:

(点击图片放大)

感谢阅读。编码愉快!

© . All rights reserved.