文件加密

Posted by 谢玄xx on October 28, 2024

1 开发背景概述

用户期望CMP软件支持结果加密。

2 加密方法

2.1 加密算法对应的第三方库调研

块加密算法比较流行的是 AES 算法,而ChaCha20-Poly1305是一种流加密算法。AEAD(Authenticated Encryption with Associated Data)加密模式集加密与认证于一体,极大地简化了加密流程,成为目前最受欢迎的加密模式:

传统加密模式(左)与AEAD加密模式(右)的对比图:

结合上述特点,本项目计划使用AEAD模式进行对称加密。AES-GCM 是目前常用的分组加密算法,但是其有一个缺点就是计算量大,导致性能开销比较大,因此我们将目光转向chacha。在 ARM 平台上,性能是 AES-GCM 的 3-4 倍[1]。
目前常用的第三方库为:

  1. OpenSSL—— AES
    OpenSSL是一个强大的开源工具,用于实现SSL和TLS协议,保障网络通信的安全。它不仅提供了加密库,还包括了命令行工具,可以用于创建证书、生成密钥、测试SSL/TLS连接等。
    主要支持功能:
    ● 加密算法支持 (Encryption Algorithm Support): 支持多种加密算法,如RSA、AES等。
    ● SSL/TLS协议实现 (SSL/TLS Protocol Implementation): 提供SSL v2/v3和TLS协议的实现。
    ● 证书处理 (Certificate Handling): 生成和管理SSL证书。
    代码地址:https://github.com/openssl/openssl
    or: https://www.openssl.org/source/index.html
    ● 优点:
    A. 纯C接口,调用方便
    ● 缺点
    a. 生成的都是动态库
    b. 结构复杂
  2. 【已编译通过并完成跨平台运行】Crypto++ —— AES/RSA (Github 4.6k stars, licensed under the Boost Software License 1.0)
    ● 优点:
    A. 支持跨平台
    B. 同时包含对称加密/非对称加密算法
    C. 是线程安全的
    ● 缺点:
    a. 不够轻量级,头文件近200个,静态库编译出来有150M左右
    代码地址:https://github.com/weidai11/cryptopp
    他们的主页:https://cryptopp.com/【选择 8.9.0 Release版本】
  3. 【已编译通过并完成跨平台运行】Libsodium (Github 12k stars, ISC License)
    是一个易于使用、现代化的软件库,旨在对常见的加密、解密、签名、哈希和密钥生成操作提供支持。它基于 NaCl(Networking and Cryptography library)的实现,但更便于使用,并添加了更多的功能和改进。
    ● 优点:
    A. 提供了简洁的 API,更加注重易用性和开发者友好性
    B. 性能经过优化,尤其在现代 CPU 上表现良好,特别是使用 ChaCha20-Poly1305 这样高效的加密算法。
    C. 相较Crypto++轻量级的多,全部文件占用空间仅20M,项目引入成本较低
    ● 缺点:
    a. 功能相对有限,仅涵盖了最常用的对称和非对称加密、哈希、消息认证码(MAC)和密钥派生函数
    代码地址:https://github.com/jedisct1/libsodium
    release版本下载链接:https://download.libsodium.org/libsodium/releases/【选择libsodium-1.0.18-msvc版本】
  4. TinyTls (支持 TLS 1.2 and TLS 1.3协议)
    https://github.com/Anthony-Mai/TinyTls
  5. Chacha20-Poly1305
    ● 优点:
    A. 超轻量级,仅需引入头文件,无需编译静态库/动态库
    ● 缺点:
    A. 由一位菲律宾学生开发,鲁棒性存疑,后期维护不确定性较大
    B. 仅支持C++20及以上标准

2.2 采用的加密算法


ChaCha20-Poly1305(Chacha20 ——对称加密算法,表示该算法有20轮的加密计算;Poly1305 ——身份认证算法)是由ChaCha20流密码和Poly1305消息认证码(MAC)结合的一种应用在互联网安全协议中的认证流加密算法,是轻量级、纯头文件形式、采用MIT协议的算法库,由 Daniel J. Bernstein 设计。它基于 Salsa20 算法,具有更强的抵抗密码分析攻击的特性。Chacha20 采用 256 位的密钥和 64 位的随机数(nonce),生成一个伪随机密钥流,然后与明文进行异或运算得到密文。由于是流密码,故以字节为单位进行加密,安全性的关键体现在密钥流生成的过程,即所依赖的伪随机数生成器的强度。 它的基本思想为:加密时,将明文数据与用户之间约定的某些数据将密钥流与明文逐字节异或,得到密文数据;由异或操作的特点可知,在解密时,只需要将密文数据与用户之间约定的那些数据再次进行异或操作,就得到了明文数据。
代码地址:https://github.com/mrdcvlsc/ChaCha20-Poly1305
ChaCha20-Poly1305采用AEAD加密模式,它在内部自行处理加密和MAC运算,无须密码库(比如OpenSSL)处理,算法精简、安全性强、兼容性强,故本项目采用此加密算法。前文所述第三方库均包含ChaCha20-Poly1305算法,因此只需调用对应的函数进行文件加密/解密操作即可,比较方便。

2.3 加密Demo展示

以引用Libsodium库为例,对指定文本:”Hello, Libsodium! \nCurrent Path: ***.”进行加密和解密操作,结果如下:
3 加密/解密模块 加密/解密模块的设计框图如下所示:
计划开发独立软件Validator,用于对文本进行加密与解密操作。由于采用对称加密的方式,因此加密/解密共享同一个密钥。考虑到客户对加密的需求并不严苛,因此计划将Key和IV值定义为const值,加密/解密都使用固定的密钥。【初版代码将key和IV值设置为32位/12位的全0值,后续可将此二值复杂化】

3.1 第三方库引入

Libsodium 是一个功能强大、易于使用的加密库,适合各种应用场景,尤其是需要高安全性和高性能的场景。其广泛的功能和良好的跨平台支持,是本次开发使用它的主要原因。

3.2 代码展示

Cmake: $ENV {LIBRARYFILES} 请替换为自己文件夹对应的libsodium库目录

cmake minimum required (VERSION 3.10)
project (MyLibsodium)
set (CMAKE CXX STANDARD 11)

if (WIN32)
  set (LIBSODIUM DIR "$ENV {LIBRARYFILES} /libsodium")
elseif (UNIX)
  set (LIBSODIUM DIR "/NFS2/yuwei.xie/libsodium")
endif ()

include directories ($ { LIBSODIUM DIR} /include)
link directories ($ { LIBSODIUM DIR} /lib)
add executable(MyLibsodium src/main.cpp)
target include directories (MyLibsodium PUBLIC include)
target link libraries (MyLibsodium "${LIBSODIUM DIR} /lib/libsodium.lib")

Demo源码:

#include <iostream>
#include <ctime>
#include <functional>
#include <algorithm>
#include <iomanip>
#include <string>
#include <vector>
#include <stack>
#include <math.h>
#include <cassert>
#include <string>
#include <fstream>

#include "sodium.h"

void encrypt_file(const std::string &input_file, const std::string &output_file,
                    const std::vector<unsigned char> &key,
                    const std::vector<unsigned char> &nonce)
{
    std::ifstream ifs(input_file, std::ios::binary);
    if (!ifs.is_open())
    {
        throw std::runtime_error("Failed to open input file.");
    }
    std::vector<unsigned char> plaintext((std::istreambuf_iterator<char>(ifs)), std::istreambuf_iterator<char>());
    ifs.close();
    
    if (plaintext.empty())
    {
        throw std::runtime_error("Input file is empty or cannot be read.");
    }

    std::vector<unsigned char> ciphertext(plaintext. size() + crypto_aead_chacha20poly1305_IETF_ABYTES);

    unsigned long long ciphertext_len;
    if (crypto_aead_chacha20poly1305_ietf_encrypt(ciphertext.data(), &ciphertext_len, plaintext.data(),
    plaintext.size(),
    nullptr, 0, // Additional data and its length (not used here)
    nullptr, nonce.data(), key.data()) != 0)
    {
        throw std::runtime_error("Encryption failed.");
    }
    std::ofstream ofs (output_file, std::ios::binary);
    if (!ofs.is_open())
    {
        throw std::runtime_error("Failed to open output file.");
    }
        
    ofs.write(reinterpret_cast<char *>(ciphertext.data()), ciphertext_len);
    ofs.close();
}

void decrypt_file(const std::string &input_file, const std::string &output_file,
                    const std::vector<unsigned char> &key,
                    const std::vector<unsigned char> &nonce)
{

    std::ifstream ifs(input_file, std::ios::binary);
    if (!ifs.is_open())
    {
        throw std::runtime_error("Failed to open input file.");
    }
    std::vector<unsigned char> ciphertext((std::istreambuf_iterator<char>(ifs)), std::istreambuf_iterator<char>());
    ifs.close();

    if (ciphertext.empty())
    {
        throw std::runtime_error("Encrypted file is empty or cannot be read.");
    }
    std::vector<unsigned char> decrypted(ciphertext.size() - crypto_aead_chacha20poly1305_IETF_ABYTES);
    unsigned long long decrypted_len;
    if (crypto_aead_chacha20poly1305_ietf_decrypt(decrypted.data(), &decrypted_len, nullptr, ciphertext.data(),
        ciphertext.size(), nullptr,
        0, // Additional data and its length (not used here)
        nonce.data(), key.data()) != 0)
    {
        throw std::runtime_error("Decryption failed.");
    }
    std::ofstream ofs(output_file, std::ios::binary);
    if (!ofs.is_open())
    {
        throw std::runtime_error("Failed to open output file.");
    }
    ofs.write(reinterpret_cast<char *>(decrypted.data()), decrypted_len);
    ofs.close();
}

int main()
{
    if (sodium_init() < 0)
    {
        std::cerr << "Failed to initialize sodium." << std::endl;
        return 1;
    }
    std::vector<unsigned char> key(crypto_aead_chacha20poly1305_IETF_KEYBYTES);
    std::vector<unsigned char> nonce(crypto_aead_chacha20poly1305_IETF_NPUBBYTES);
    //randombytes_buf(key.data(), key.size());
    //randombytes_buf(nonce.data(), nonce.size());
    try
    {
        std::string input_file ="D:\\CMP_Files\\cipherPath\\Recipe.rcp";
        std::string encrypted_file = "D:\\CMP_Files\\cipherPath\ \Recipe_jiami.rcpx";
        std::string decrypted_file = "D:\\CMP_Files\\cipherPath\\Recipe_jiemi.rcp";
        encrypt_file(input_file, encrypted_file, key, nonce);
        decrypt_file(encrypted_file, decrypted_file, key, nonce);
        std::cout << "Encryption and decryption completed successfully." << std::endl;
    }
    catch (const std::exception &e)
    {
        std::cerr << e.what() << std::endl;
        return 1;
    }
    return 0;
}

4 参考链接/文献

[1] https://wuxuejun2018.github.io/2019/04/25/ChaCha20-Poly1305/
[2] https://hackernoon.com/understanding-cipher-suites-and-aead-chacha20-poly1305-example
[3] https://doc.libsodium.org/