嵌入式系统与单片机|技术阅读
登录|注册

您现在的位置是:嵌入式系统与单片机 > 技术阅读 > 极简加解密库--使数据传输更加安全

极简加解密库--使数据传输更加安全

概述

  • 互联网时代,安全成为了一个沉重的话题。文件传输、电子邮件等的安全性尤为重要。我们为了保证安全性,必须对其内容加密,
  • 加密的作用就是防止有用或私有化信息在传输链路上被拦截和窃取。提高数据传输的可靠性。
  • 在嵌入式开发中,我们会涉及到数据的传输,文件的传输。很多人都没有考虑其数据的安全性问题,往往都是明文的方式传输,最多增加CRC进行数据的完整性校验。这明显没有考虑数据的安全性问题。
  • 最近项目遇到安全性问题,所以也开始折磨一下加密相关知识,发现RT-THREAD有个比较好软件包--tinycrypt(一个简小并且可配置的加解密软件包,包含算法:aes,base64,md5,sha1,sha2)。其中:AES刚好符合我的项目需求。

AES加密算法

AES加密标准又称为高级加密标准Rijndael加密法,是美国国家标准技术研究所NIST旨在取代DES的21世纪的加密标准。AES的基本要求是,采用对称分组密码体制,密钥长度可以为128、192或256位,分组长度128位。AES算法是最为常见的额对称加密算法之一。

AES加密流程说明:

AES的加解密流程图如下:

加解密流程图部件说明:

  • 明文P:没有经过加密的数据或文件。
  • 密钥K:用来加密明文P的密钥,在对称加密算法中,加密与解密的密钥是相同的。密钥为接收方与发送方协商产生,但不可以直接在网络上传输,否则会导致密钥泄漏,通常是通过非对称加密算法加密密钥,然后再通过网络传输给对方,或者直接面对面商量密钥。密钥是绝对不可以泄漏的,否则会被攻击者还原密文,窃取机密数据。
  • AES加密函数:设AES加密函数为E,则 C = E(K + P),其中P为明文,K为密钥,C为密文。也就是说,把明文P和密钥K作为加密函数的参数输入,则加密函数E会输出密文C。
  • 密文C:通过密钥对明文进行加密处理后的数据或文件。
  • AES解密函数:设AES解密函数为D,则 P = D(K + C),其中C为密文,K为密钥,P为明文。也就是说,把密文C和密钥K作为解密函数的参数输入,则解密函数会输出明文P。

AES加密方法:

  • AES为分组加密,分组加密也就是把明文分成一组一组的,每组的长度相等,每次加密一组数据,直到加密完整个明文。
  • AES密钥长度(32bit)分组长度(32bit)加密轮数
    AES-1284410
    AES-1926412
    AES-2568414
  • 在AES标准规范中,分组长度只能是128位,也就是说,每个分组为16个字节,每个字节8位,密钥的长度可以使用128位、192位或者258位。密钥的长度不同,推荐加密轮数也不同,比如AES-128也就是密钥的长度为128位,加密轮数为10轮,AES-192为12轮,AES-256为14轮。以AES-128为例,加密中一轮的4个操作:
    • 字节代换:AES的字符代换其实就是一个简单的查表操作,AES定义了一个S盒和一个逆S盒。
    • 行位移:就是一个简单的左循环移位操作。
    • 列混合:是通过矩阵相乘来实现的,经过移位后的状态矩阵与固定的矩阵相乘,得到混淆后的状态矩阵。
    • 轮密钥加:是将128位轮密钥Ki同状态矩阵中的数据进行逐位异或操作。
  • 加密:加密第1轮到第9轮的轮函数一样,最后一轮迭代不执行列混合,另外,在第一轮迭代之前,先将明文和原始密钥进行一次异或加密操作。
  • 解密:解密过程仍为10轮,每一轮的操作是加密操作的逆操作。由于AES的4个轮操作都是可逆的,因此,解密操作的一轮就是顺序执行逆行移位、逆字节代换、轮密钥加和逆列混合。同加密操作类似,最后一轮不执行逆列混合,在第1轮解密之前,要执行1次密钥加操作。
  • tinycrypt的AES使用

  • tinycrypt这个软件包精简使用起来很方便,其包含了源码和测试用例(AES和MD5),tinycrypt软件包的目录结构如下:
  • .
    ├── include
    │   ├── tiny_aes.h                  // aes 头文件
    │   ├── tiny_base64.h               // base64 头文件
    │   ├── tinycrypt_config.h          // tinycrypt 配置头文件
    │   ├── tinycrypt.h                 // tinycrypt 头文件
    │   ├── tiny_md5.h                  // md5 头文件
    │   ├── tiny_sha1.h                 // sha1 头文件
    │   └── tiny_sha2.h                 // sha2 头文件
    ├── LICENSE
    ├── README.md
    ├── samples
    │   ├── aes_sample.c                // aes 测试用例头文件
    │   └── md5_sample.c                // md5 测试用例头文件
    └── src
        ├── tiny_aes.c                  // aes 源文件
        ├── tiny_base64.c               // base64 源文件
        ├── tiny_md5.c                  // md5 头文件
        ├── tiny_sha1.c                 // sha1 头文件
        └── tiny_sha2.c                 // sha2 头文件
  • 在Ubuntu环境下,搭建tinycrypt的环境,测试加解密文件,目录结构如下:
  • .
    ├── main.c
    ├── makefile
    └── tinycrypt
        ├── include
        │   ├── tiny_aes.h
        │   ├── tiny_base64.h
        │   ├── tinycrypt_config.h
        │   ├── tinycrypt.h
        │   ├── tiny_md5.h
        │   ├── tiny_sha1.h
        │   └── tiny_sha2.h
        ├── LICENSE
        ├── README.md
        ├── samples
        │   ├── aes_sample.c
        │   └── md5_sample.c
        └── src
            ├── tiny_aes.c
            ├── tiny_base64.c
            ├── tiny_md5.c
            ├── tiny_sha1.c
            └── tiny_sha2.c
    • 在目录中增加makefile文件和main.c,其中makefile用于构建工程,main.c用于编写我们的测试代码,其中:

    • makefile文件内容:

    VERSION = 1.0.0                                             # 版本号

    SOURCE  = main.c                                            # 源文件
    SOURCE += $(wildcard ./tinycrypt/src/*.c)                   # 通过wildcard函数获取tinycrypt源文件
    OBJECT  = $(patsubst %.c, %.o, $(SOURCE))                   # 通过patsubst函数替换源文件为目标文件

    INCLUDE = -I ./tinycrypt/include                            # 包含头文件路径

    TARGET  = tinycrypt                                         # 目标文件名字
    CC      = gcc                                               # 工具链
    CFLAGS  = -Wall -g                                          # 编译选项

    OUTPUT  = output                                            # 输出目录

    $(TARGET): $(OBJECT)                                        # 构建执行命令
     @mkdir -p $(OUTPUT)                                     # 创建输出目录
     $(CC) $^ $(CFLAGS) -o $(OUTPUT)/$(TARGET)_$(VERSION)    # 编译

    %.o: %.c                                                    # 将源文件编译成目标文件
     $(CC) $(INCLUDE) $(CFLAGS) -c $< -o $@                  

    .PHONY:clean                                                # 执行make clean之后,清除过程文件
    clean:
     @rm -rf $(OBJECT) $(OUTPUT)
    • main.c文件内容,测试历程说明:将一个文件(index.js)通过AES加密,然后生成加密文件(encrypt_index.js),再将加密文件(encrypt_index.js)通过AES解密,生成解密文件(decrypt_index.js)。
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <stdint.h>

    #include <sys/types.h>
    #include <sys/stat.h>

    #include <fcntl.h>
    #include <unistd.h>

    #include "tiny_aes.h"

    #define ENCRYPT_TEST_FILE     "./index.js"
    #define ENCRYPT_TEST_ENCRYPT_FILE   "./encrypt_index.js"
    #define ENCRYPT_TEST_DECRYPT_FILE   "./decrypt_index.js"

    #define ENCRYPT_SINGLE_SIZE    1024                                    // 加密单包长度

    #define ENCRYPT_AES_IV_LEN     16                                      // AES向量长度
    #define ENCRYPT_AES_KEY_LEN    256                                     // AES密钥长度
    #define ENCRYPT_AES_IV      "0123456789ABCDEF"                      // AES向量
    #define ENCRYPT_AES_KEY     "0123456789ABCDEF0123456789ABCDEF"      // AES密钥
    // AES参数结构体定义
    typedef struct
    {

     uint8_t aesIv[ENCRYPT_AES_IV_LEN];
     uint8_t aesKey[ENCRYPT_AES_KEY_LEN];
     tiny_aes_context aes_ctx;
    } EncryptInfo;

    static EncryptInfo g_info = {0};
    // aes 参数初始化
    void aes_init(const char *iv, const char *key)
    {
     memset(g_info.aesIv, 0, ENCRYPT_AES_IV_LEN);
     memset(g_info.aesKey, 0, ENCRYPT_AES_KEY_LEN);
     if (iv == NULL) {
      memcpy(g_info.aesIv, ENCRYPT_AES_IV, strlen(ENCRYPT_AES_IV));
     }
     else {
      memcpy(g_info.aesIv, iv, strlen(iv));
     }
     if(key == NULL) {
      memcpy(g_info.aesKey, ENCRYPT_AES_KEY, strlen(ENCRYPT_AES_KEY));
     }
     else {
      memcpy(g_info.aesKey, key, strlen(key));
     }
    }
    // aes加密函数的封装
    void aes_encrypt(uint8_t *dest_buff, const uint8_t *src_buff, uint32_t len)
    {
     tiny_aes_crypt_cbc(&g_info.aes_ctx, AES_ENCRYPT, len, g_info.aesIv, (uint8_t *)src_buff, dest_buff);
    }
    // aes解密函数的封装
    void aes_decrypt(uint8_t *dest_buff, const uint8_t *src_buff, uint32_t len)
    {
     tiny_aes_crypt_cbc(&g_info.aes_ctx, AES_DECRYPT, len, g_info.aesIv, (uint8_t *)src_buff, dest_buff);
    }

    int main(int argc, char *argv[])
    {
     int raw_size = 0;
     int src_fd = -1;
     int encrypt_fd = -1;
     int decrypt_fd = -1;
     int src_file_size = 0;
     int srv_file_offset = 0;
     uint8_t src_buff[ENCRYPT_SINGLE_SIZE];
     uint8_t encrypt_buff[ENCRYPT_SINGLE_SIZE];
     uint8_t decrypt_buff[ENCRYPT_SINGLE_SIZE];

     // 加密文件,将明文文件(index.js)通过AES加密为加密文件(encrypt_index.js)
        {
            aes_init(NULLNULL);                                                                   // aes 初始化
            tiny_aes_setkey_enc(&g_info.aes_ctx, (uint8_t *)g_info.aesKey, ENCRYPT_AES_KEY_LEN);    // 设置密钥
            src_fd = open(ENCRYPT_TEST_FILE, O_RDONLY, 0777);                                       // 打开明文文件
            if (src_fd == -1) {
                printf("%d-ERR: open file(%s) failed\r\n", __LINE__, ENCRYPT_TEST_FILE);
                return -1;
            }

            encrypt_fd = open(ENCRYPT_TEST_ENCRYPT_FILE, O_RDWR | O_CREAT | O_TRUNC, 0777);         // 打开加密文件
            if (encrypt_fd == -1) {
                printf("%d-ERR: open file(%s) failed\r\n", __LINE__, ENCRYPT_TEST_ENCRYPT_FILE);
                return -1;
            }

            raw_size = lseek(src_fd, 0, SEEK_END);                                                  // 获取明文文件的大小
            srv_file_offset = 0;
            lseek(src_fd, 0, SEEK_SET);
            printf("%d-INFO: source file size: %d\r\n", __LINE__, raw_size);

            while (srv_file_offset < raw_size) {
                memset(src_buff, 0, ENCRYPT_SINGLE_SIZE);
                memset(encrypt_buff, 0, ENCRYPT_SINGLE_SIZE);
                read(src_fd, src_buff, ENCRYPT_SINGLE_SIZE);                                        // 读取明文文件内容
                aes_encrypt(encrypt_buff, src_buff, ENCRYPT_SINGLE_SIZE);                           // 明文文件内容加密
                write(encrypt_fd, encrypt_buff, ENCRYPT_SINGLE_SIZE);                               // 将加密内容写入加密文件中
                srv_file_offset += ENCRYPT_SINGLE_SIZE;
            }
            close(src_fd);
            src_fd = -1;
            close(encrypt_fd);
            encrypt_fd = -1;
        }

     // 解密文件,将加密文件(encrypt_index.js)通过AES解密为解密文件(decrypt_index.js)
        {
            aes_init(NULLNULL);                                                                   // aes 初始化
            tiny_aes_setkey_dec(&g_info.aes_ctx, (uint8_t *)g_info.aesKey, ENCRYPT_AES_KEY_LEN);    // 设置密钥
            src_fd = open(ENCRYPT_TEST_ENCRYPT_FILE, O_RDONLY, 0777);                               // 打开密文文件
            if (src_fd == -1) {
                printf("%d-ERR: open file(%s) failed\r\n", __LINE__, ENCRYPT_TEST_ENCRYPT_FILE);
                return -1;
            }

            decrypt_fd = open(ENCRYPT_TEST_DECRYPT_FILE, O_RDWR | O_CREAT | O_TRUNC, 0777);         // 打开解密文件
            if (decrypt_fd == -1) {
                printf("%d-ERR: open file(%s) failed\r\n", __LINE__, ENCRYPT_TEST_DECRYPT_FILE);
                return -1;
            }

            src_file_size = raw_size;                                                               // 设置明文文件的大小
            srv_file_offset = 0;
            lseek(src_fd, 0, SEEK_SET);
            printf("%d-INFO: encrypt file size: %d\r\n", __LINE__, src_file_size);

            while (srv_file_offset < src_file_size) {
                memset(src_buff, 0, ENCRYPT_SINGLE_SIZE);
                memset(decrypt_buff, 0, ENCRYPT_SINGLE_SIZE);
                if((srv_file_offset + ENCRYPT_SINGLE_SIZE) < src_file_size)
                {
                    read(src_fd, src_buff, ENCRYPT_SINGLE_SIZE);                                    // 读取密文文件内容
                    aes_decrypt(decrypt_buff, src_buff, ENCRYPT_SINGLE_SIZE);                       // 密文文件解密
                    write(decrypt_fd, decrypt_buff, ENCRYPT_SINGLE_SIZE);                              // 将解密内容写入解密文件中
                    srv_file_offset += ENCRYPT_SINGLE_SIZE;
                }
                else
                {
                    read(src_fd, src_buff, ENCRYPT_SINGLE_SIZE);                                    // 读取最后一包密文文件内容
                    aes_decrypt(decrypt_buff, src_buff, (ENCRYPT_SINGLE_SIZE));                     // 密文文件解密
                    write(decrypt_fd, decrypt_buff, (src_file_size - srv_file_offset));             // 将最后一包解密内容写入解密文件中
                    break;
                }
            }
            close(src_fd);
            src_fd = -1;
            close(decrypt_fd);
            decrypt_fd = -1;
        }

     return 0;
    }
  • 验证时发现一个问题,加密内容的长度需要16字节的整数倍,所以加密完的文件大小也为16字节的整数倍。值得注意的是,解密的最后一包要根据明文文件的大小算出来的,然后写进解密文件中,因为加密文件是16字节对齐的,所以要去除16字节对齐。

  • 编译及运行:

  • rice@rice:~/project/encrypt$ make
    gcc -I ./tinycrypt/include -Wall -g -c main.c -o main.o
    gcc -I ./tinycrypt/include -Wall -g -c tinycrypt/src/tiny_sha1.c -o tinycrypt/src/tiny_sha1.o
    gcc -I ./tinycrypt/include -Wall -g -c tinycrypt/src/tiny_aes.c -o tinycrypt/src/tiny_aes.o
    gcc -I ./tinycrypt/include -Wall -g -c tinycrypt/src/tiny_md5.c -o tinycrypt/src/tiny_md5.o
    gcc -I ./tinycrypt/include -Wall -g -c tinycrypt/src/tiny_sha2.c -o tinycrypt/src/tiny_sha2.o
    gcc -I ./tinycrypt/include -Wall -g -c tinycrypt/src/tiny_base64.c -o tinycrypt/src/tiny_base64.o
    gcc main.o tinycrypt/src/tiny_sha1.o tinycrypt/src/tiny_aes.o tinycrypt/src/tiny_md5.o tinycrypt/src/tiny_sha2.o tinycrypt/src/tiny_base64.o -Wall -g -o output/tinycrypt_1.0.0
    rice@rice:~/ohos/project/encrypt$ ./output/tinycrypt_1.0.0 
    136-INFO: source file size: 445
    171-INFO: encrypt file size: 445
    rice@rice:~/ohos/project/encrypt$ 
  • 演示结果:
  • 结论:

  • 通过加密方式,提高数据的安全性,避免了数据的泄露
  • AES加密方式,加密内容的长度需要16字节的整数倍,加密之后长度比实际明文可能要长。所以明文的长度需要告知解密方。
  • AES是对称加密,所以加密方和解密方的密钥需要一直。