Daxia Blog
Uncategorized | Rust | WebUI | FHIR | Javascript | KB

数据的加解密

这不是一篇介绍加密算法的文章。主要记录下一些常见加密算法的Rust实现。

缘起

系统的某些数据资产需要下载到客户端使用,但是需要对这些数据资产进行加密处理。在客户端存储的是加密后的数据。运行时,通过客户端的私钥进行解密后加载到内存中使用。

思路就是:采用混合加密。用对称加密算法来加密数据,用非对称加密算法来加密对称算法的密钥。这也是网络通讯通常采用的加密方式。

服务器持有公钥,客户端持有私钥。每次客户端向服务端请求数据时,服务端都会动态生成一个对称算法的密钥,用该密钥对数据加密,然后用非对称加密算法来加密该密钥。服务端将加密数据和加密密钥一起传送给客户端,客户端就可以结合自己手里的私钥对密钥进行解密,再用对称密钥解密数据。

同时,为了便于传输,还需要使用Base64对密钥和数据进行编解码。

对称加密算法的选择

在现代密码学中,有几种常用的对称加密算法:

  • AES(Advanced Encryption Standard)
    • 最常用的对称加密算法,性能高、安全性强。
  • ChaCha20
    • 现代流密码算法,广泛用于移动和网络应用。在TLS、HTTPS等协议中使用。

实践中,AES的Rust实现太难了,一些示例所使用的依赖过期,没调试通过。所以,直接使用了Chacha20,拿来即用。

首先引入依赖项:

[dependencies]
rand = "0.8.5"
chacha20 = "0.9.1"

如下,提供了两个封装函数:随机生成对称密钥和利用对称算法对数据进行加解密:

use chacha20::ChaCha20;
use chacha20::cipher::{
    KeyIvInit,
    StreamCipher,
    KeyInit
};

// 定义加密常量
const CHACHA_KEY_SIZE: usize = 32;  // ChaCha20密钥长度
const NONCE_SIZE: usize = 12;       // Nonce大小


// 生成ChaCha20密钥和Nonce
fn generate_chacha20_key() -> Vec<u8> {
    let mut rng = rand::rngs::OsRng;
    let mut full_key = [0u8; CHACHA_KEY_SIZE + NONCE_SIZE];
    rng.fill_bytes(&mut full_key);
    full_key.to_vec()
}

// 对称算法,加解密都是同样的方法
fn chacha20_code(mut data: Vec<u8>, full_key: &[u8]) -> anyhow::Result<Vec<u8>> {
    // 分割密钥和Nonce
    let key = &full_key[..CHACHA_KEY_SIZE];
    let nonce = &full_key[CHACHA_KEY_SIZE..CHACHA_KEY_SIZE+NONCE_SIZE];

    // 使用ChaCha20加密文件内容
    let mut cipher = ChaCha20::new(key.into(), nonce.into());
    cipher.apply_keystream(&mut data);

    Ok(data)
}

非对称算法的选择

非对称加密算法(也称为公钥加密算法)是现代密码学中非常重要的一类算法。以下是几种主要的非对称加密算法:

  • RSA(Rivest-Shamir-Adleman)
    • 最广泛使用的非对称加密算法之一。基于大数因数分解的困难性,既可以用于加密,也可以用于数字签名。安全性依赖于大素数的乘积很难被因数分解。
  • ECC(椭圆曲线密码学)
    • 相对RSA,密钥更短,计算效率更高,在移动和物联网设备上应用广泛。安全性基于椭圆曲线上离散对数问题的困难性。

在这里选择了RSA加密算法,首先引入依赖项:

[dependencies]
rsa = "0.9.7"

非对称加密算法主要用来对对称加密密钥进行加解密:

use rsa::{Pkcs1v15Encrypt, RsaPrivateKey, RsaPublicKey};

// 加密ChaCha20密钥
fn encrypt_chacha20_key(public_key: &RsaPublicKey, encrypted_key: &[u8]) -> anyhow::Result<Vec<u8>> {
    let mut rng = rand::rngs::OsRng;
    let encrypted_aes_key = public_key.encrypt( &mut rng, Pkcs1v15Encrypt, encrypted_key)?;
    Ok(encrypted_aes_key)
}

// 解密ChaCha20密钥
fn decrypt_chacha20_key(private_key: &RsaPrivateKey, encrypted_key: &[u8]) -> anyhow::Result<Vec<u8>> {
    let decrypted_aes_key = private_key.decrypt(Pkcs1v15Encrypt, encrypted_key)?;
    Ok(decrypted_aes_key)
}

编码方式的选择

字节流还是不便于传输的,需要用Base64将字节流编码为字符串以便于传输:

[dependencies]
base64 = "0.22.1"

密钥在生成时,是一个字节数组,也是需要进行Base64编码的。不过,在传输前,需要用非对称算法对密钥进行加密:

use base64::prelude::*;

fn key_encode(public_key: &RsaPublicKey, key: &[u8]) -> anyhow::Result<String> {
    // rsa encrypt
    let temp = encrypt_chacha20_key(&public_key, key)?;
    // base64 encode
    let temp = BASE64_STANDARD.encode(&temp);

    Ok(temp)
}

fn key_decode(private_key: RsaPrivateKey, encode_key: String) -> anyhow::Result<Vec<u8>> {
    // base64 decode
    let temp = BASE64_STANDARD.decode(encode_key.as_bytes())?;
    // rsa decrypt
    let temp = decrypt_chacha20_key(&private_key, &temp)?;

    Ok(temp)
}

对数据的Base64编解码:

n resource_encode(resource: Vec<u8>, key: &[u8]) -> anyhow::Result<String> {
    // chacha20 encrypt
    let temp = chacha20_code(resource, key)?;
    // base64 encode
    let temp = BASE64_STANDARD.encode(&temp);

    Ok(temp)
}

fn resource_decode(resource: Vec<u8>, key: &[u8]) -> anyhow::Result<String> {
    // base64 decode
    let temp = BASE64_STANDARD.decode(&resource)?;
    // rsa decrypt
    let temp = chacha20_code(temp, key)?;
    // bytes to string
    let temp = String::from_utf8(temp)?;

    Ok(temp)
}

私钥的存储

非对称加密的私钥将以符合PKCS#8的标准格式进行存储和读取:

fn generate_pkcs8(private_key: &RsaPrivateKey, path: &PathBuf) -> anyhow::Result<()> {
    private_key.write_pkcs8_pem_file(path, LineEnding::LF)
        .map_err(|e| anyhow::anyhow!(e.to_string()))
}

fn retrieve_pkcs8(path: &PathBuf) -> anyhow::Result<RsaPrivateKey> {
    let content = fs::read_to_string(path)?;
    RsaPrivateKey::from_pkcs8_pem(&content)
        .map_err(|e| anyhow::anyhow!(e.to_string()))
}

私钥是极其敏感的信息,必须得到妥善保管和保护。

最后给出完整示例

示例中的公私钥是临时生成的。实践中公私密钥对可以通过多种途径生成并应妥善保管。

fn main() -> anyhow::Result<()> {
    let mut rng = rand::rngs::OsRng;

    let bits = 2048;
    let private_key = RsaPrivateKey::new(&mut rng, bits).expect("failed to generate a key");
    let public_key = RsaPublicKey::from(&private_key);

    // 服务端加密
    let content = fs::read("test.xml")?;
    let chacha20_key = generate_chacha20_key();
    let encode_data = resource_encode(content, &chacha20_key)?;
    let encode_key = key_encode(&public_key, &chacha20_key)?;
    println!("Data base64: {}", &encode_data);
    println!("Key base64: {}", &encode_key);

    // 客户端解密
    let chacha20_key2 = key_decode(private_key, encode_key)?;
    let decode_data = resource_decode(encode_data.into_bytes(), &chacha20_key2)?;
    println!("Raw Data: {}", &decode_data);

    Ok(())
}

About Daxia
我是一名独立开发者,国家工信部认证高级系统架构设计师,在健康信息化领域与许多组织合作。具备大型卫生信息化平台产品架构、设计和开发的能力,从事软件研发、服务咨询、解决方案、行业标准编著相关工作。
我对健康信息化非常感兴趣,尤其是与HL7和FHIR标准的健康互操作性。我是HL7中国委员会成员,从事FHIR培训讲师和FHIR测评现场指导。
我还是FHIR Chi的作者,这是一款用于FHIR测评的工具。