使用AES CTR模式实现任意位置数据加解密

目录

需求

AES加密

CTR模式计数器

完整demo实现

openssl命令行进行验证


需求

最近对我们的项目进行安全优化,我们的服务程序在使用过程中会持续接受客户端的一些数据并追加写到文件中,文件写入完毕后还支持客户端的随机访问来获取这些结构化信息。之前数据的写入都是明文方式,因此考虑使用最广泛使用的AES对称加密来进行处理,我们想到了两种方式进行:1)写入完毕后另起一个线程/进程读文件->加密->写文件->删除旧文件。2)写入数据时就进行加密。第一种方式虽然简单,但是在加密完成前还是存在泄露的风险,因此我们选择第二种方法进行。在实现中发现有以下难点:1)当前加密大都是对完整数据进行,缺少动态数据的加密。2)解密时因为要支持随机访问,所以解密要支持从任何位置的任意长度解密。在对AES的加密学习了解的基础上,最终解决这些问题,在此进行记录。

AES加密

AES加密及模式的学习参考了下面两篇文章:

  1. 一文搞懂对称加密:加密算法、工作模式、填充方式、代码实现:https://juejin.cn/post/7030953914509836296
  2. AES的CTR模式加密解密详解:AES的CTR模式加密解密详解 | Wuman's Blog

其中支持从任意字节解密的模式有ECB、CBC、CFB和CTR,但是ECB不能抵抗重放攻击进行排除,而CBC、CFB在解密时都需要知道前一个分组的密文,在对文件进行随机访问时这需要往前多读取一些数据,使用不方便进行排除,最终只剩下CTR模式。

CTR模式计数器

CTR模式的关键是计数器,如果我们知道任意位置的计数器那么就可以进行解密,由于CTR模式的计数器是按照分组递增的,所以我们只要知道是哪一个分组即可,而AES进行加解密的分组大小不管是128位还是其他都是16字节,所以可以根据读取的数据所在的字节位置/16知道所在哪一个分组,然后再加上初始的计算器就得到了该分组的计数器了,之后解密就很简单了。如果数据长度大于16字节,说明包含多个分组,依次计算每一个分组的计数器即可,这样我们就实现了从任意位置开始的任意长度解密;

因为CTR模式加密和解密的算法是完全一样的,所以实际上也就同时实现了任意位置开始任意长度的加密和解密。

完整demo实现

下面使用openssl实现AES CTR(128位)模式任意位置任意长度的加解密,这个demo使用的是低层次的API,我认为高层次的API不太好实现(因为不能传递偏移量,所以需要进行额外数据读取来保证对齐)。

#include 
#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;


uint64_t htonll(uint64_t host64) {
    uint32_t high_part = htonl((uint32_t)(host64 >> 32));
    uint32_t low_part = htonl((uint32_t)(host64 & 0xFFFFFFFF));
    return (((uint64_t)low_part) << 32) | high_part;
}

// 从最后一个字节开始加1,如果最后一个字节变为0,则加前一个字节;
void increment_counter(unsigned char* counter, int len) {
    for (int i = len - 1; i >= 0; --i) {
        if (++counter[i]) {
            break;
        }
    }
}

void aes_ctr_encrypt(const unsigned char *plaintext, int start_offset, int plaintext_len, const unsigned char *key,
                     const unsigned char *iv, unsigned char *ciphertext) {
    AES_KEY aes_key;
    AES_set_encrypt_key(key, 128, &aes_key); /*设置AES密钥*/

    unsigned char ctr[AES_BLOCK_SIZE]; /*计数器*/
    unsigned char stream_block[AES_BLOCK_SIZE]; /*计数器和密钥加密后的结果*/
    memcpy(ctr, iv, AES_BLOCK_SIZE);

    int block_offset = start_offset / AES_BLOCK_SIZE; /*计算从哪个块开始*/
    int within_block_offset = start_offset % AES_BLOCK_SIZE; /*计算从块内的哪个位置开始*/
    int output_index = 0; /*输出明文的索引*/
    int remaining_len = plaintext_len; /*需要处理的数据长度*/

    // 调整计数器的值,使得对每一个16字节的块其值都是不一样的,并且和解密的规则是一样的;和下面注释的代码功能是一样的,但需要的运算更少
    uint64_t *p = (uint64_t*)(ctr) + 1;
    (*p) += block_offset;
    (*p) = htonll(*p);
    /*
    for (int i = 0; i < block_offset; i++) 
        increment_counter(ctr, AES_BLOCK_SIZE);
    */

    // 初始计数器值加密
    AES_encrypt(ctr, stream_block, &aes_key); 

    // 从指定的起始位置开始加密指定长度的数据
    while (remaining_len > 0) {
        int bytes_to_process = std::min(AES_BLOCK_SIZE - within_block_offset, remaining_len);
        for (int i = 0; i < bytes_to_process; ++i) {
            ciphertext[output_index++] = plaintext[output_index] ^ stream_block[within_block_offset + i];
        }
        within_block_offset = 0; /*下一个块从第一个字节开始*/
        remaining_len -= bytes_to_process; /*更新剩余数据长度*/

        // 更新计数器值及计算器加密
        if (remaining_len > 0) {
            increment_counter(ctr, AES_BLOCK_SIZE);
            AES_encrypt(ctr, stream_block, &aes_key); 
        }
    }
}

void aes_ctr_decrypt(const unsigned char *ciphertext, int start_offset, int ciphertext_len, const unsigned char *key,
                     const unsigned char *iv, unsigned char *plaintext) {
    aes_ctr_encrypt(ciphertext, start_offset, ciphertext_len, key, iv, plaintext);
}

int main(void) {
    unsigned char key[16] = {0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef};
    unsigned char iv[AES_BLOCK_SIZE] = {0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};

    // 明文字符串
    const char* plaintext = "This is a test message. OpenSSL 1.1.1 and 1.1.0 only provide AES_encrypt. It is a software only implementation. It operates on full blocks. You need to manage the counter, provide the increments, encrypt the counter, and XOR the plain text.\n";
    int plaintext_len = strlen(plaintext);
    std::cout << "数据长度为:" << plaintext_len << std::endl;

    unsigned char ciphertext[256]; /*存储加密后的内容*/
    unsigned char decryptedtext[256]; /*存储解密后的内容*/
    // 加密
    aes_ctr_encrypt(reinterpret_cast(plaintext), 0, plaintext_len, key, iv, ciphertext);
    std::cout << "加密后的数据: ";
    for (int i = 0; i < plaintext_len; i++) {
        std::cout << std::hex << std::setfill('0') << std::setw(2) << static_cast(ciphertext[i]);
    }
    std::cout << std::dec << std::endl;

    // 解密
    int start_offset = 17; /*从第17个字节开始解密*/
    int len = 50; /*解密50个字节*/
    aes_ctr_decrypt(ciphertext + start_offset, start_offset, len, key, iv, decryptedtext);
    std::cout << "解密后的数据: " << decryptedtext << std::endl;
    return 0;
}

openssl命令行进行验证

openssl命令行工具也可直接进行完整加解密的验证,将需要加密的数据写入到in.txt文件中然后使用下面的命令即可验证我们上面程序的正确性:

openssl enc -aes-128-ctr -in in.txt -out out.enc -K 0123456789abcdef0123456789abcdef -iv 0123456789abcdef
xxd out.enc

你可能感兴趣的:(linux)