是时候用ED25519替代RSA了:Rust库`crypto_box`实践

背景

在密码学的世界里,算法的更新迭代就像一场永不停歇的竞赛。

曾经,RSA 算法凭借其可靠的安全性和广泛的应用场景,成为了加密领域的 "扛把子"。

但随着技术的飞速发展,ED25519 算法逐渐崭露头角,展现出更高效、更安全的特性。

今天,咱们就深入探讨为什么是时候用 ED25519 替代 RSA,同时结合 Rust 语言中的crypto_box库,带大家进行一波超实用的应用实践!

RSA 的辉煌与困境

RSA 算法基于大整数分解的难题,通过生成一对公私钥来实现加密和解密、数字签名和验证等功能。

自 1977 年诞生以来,RSA 广泛应用于 HTTPS、数字证书、电子签名等领域,在互联网安全的早期阶段,撑起了加密通信的一片天。

然而,RSA 的 "辉煌" 背后也暗藏 "危机"。

一方面,RSA 的密钥长度较长,加密和解密的计算复杂度高,这就导致在处理大量数据时,性能表现不尽如人意。 另一方面,随着计算能力的提升,尤其是量子计算的发展,RSA 面临着被破解的潜在风险。

ED25519 的崛起与优势

ED25519 是一种基于椭圆曲线数字签名算法(ECDSA)的算法,它采用了爱德华兹曲线(Edwards curve)。

相比 RSA,ED25519 有着显著的优势。

从性能角度来看,ED25519 的密钥生成、签名和验证过程都更加高效。它的密钥长度相对较短,但安全性却毫不逊色。

在安全性方面,ED25519 基于椭圆曲线离散对数问题,目前还没有找到针对它的有效量子攻击方法。这意味着在量子计算时代,ED25519 依然能为数据安全保驾护航 。而且,ED25519 的签名长度固定,进一步增强了其安全性和可预测性。

Rust 库 crypto_box 助力 ED25519 实践

Rust 语言以其内存安全、高性能和并发性强等特点,成为了众多开发者在安全领域的首选。

而crypto_box库则是 Rust 生态中用于实现加密通信的强大工具,它基于curve25519和xsalsa20poly1305等密码学原语,同时也支持 ED25519 算法。

接下来,咱们就通过实际代码,看看如何使用crypto_box库应用 ED25519。

此文源于一个端到端加密的项目,众所周知,既然是端到端加密,也就是客户端和服务器端都需要加密过程,算法和实现逻辑需要保持一致。

选型最后采用了ED25519,由于前后端语言不同,因此需要分别找到匹配的第三方库,然而遍寻资料却未果。

最后采用的方案是用基于Rust库crypto_box封装了一个crate,分别导出其他语言的包,来统一算法和实现逻辑。

实现过程

端到端加密并不是完全依赖非对称加密,而是结合对称加密一起实现端到端加密过程。 这个封装的库选择了crypto_boxaes-gcm分别用于非对称加密和对称加密。

crypto_box需要加解密双方各自持有一对公钥和私钥,参考RSA密钥对。 加解密和签名时需要用到对方的公钥和自己的私钥。

aes-gcm加解密共用相同的密钥key,同时每次加密都用不同的nonce防止重放攻击。

a. 默认模式创建Rust Crate

cargo new --lib $MYCRATE$

b. 添加加密库依赖

ini 复制代码
[dependencies]  
anyhow = { version = "1.0.96" }  
aes-gcm = { version = "0.10.3", features = ["alloc", "heapless"]}  
argon2 = { version = "0.5.3" }  
base64 = { version = "0.22.1" }  
crypto_box = { version = "0.9.1", features = ["chacha20"] }  
ed25519-dalek = { version = "2.0.0", features = ["serde", "rand_core", "zeroize", "pkcs8", "pem"] }  
sha3 = { version = "0.10.8" }

c. argon2-hash函数

aes算法需要固定长度的密钥key,而用户密码的长度是不固定的,项目中采用的方法是对密码进行hash,取固定长度字节,用于aes的密钥key。

常用密码hash一般用pbkdf2,调研后采用了另一种较新的argon2算法,本人非专业人员,只是查阅了资料后的选型,性能对比不做讨论。

argon2_password_hash函数主要作用是把不固定长度密码字符串进行hash后输出固定长度的字节数组。

hash.rs

rust 复制代码
use anyhow::Result;  
use argon2::{Argon2, PasswordHasher};  
use argon2::password_hash::SaltString;  
use base64::Engine;  
use base64::prelude::BASE64_STANDARD;  
use sha3::{Sha3_512, Digest};  
  
pub fn argon2_password_hash(password: &[u8]) -> Result<Vec<u8>> {  
    let mut hasher = Sha3_512::new();  
    hasher.update(password);  
    let hash = hasher.finalize();  
    let argon2_salt = SaltString::from_b64(BASE64_STANDARD.encode(hash.as_slice())[..64].trim()).unwrap();  
    let argon2 = Argon2::default();  
    let password_hash = argon2.hash_password(password, &argon2_salt).unwrap();  
    let hash_output = password_hash.hash.unwrap();  
    Ok(hash_output.as_bytes().to_vec())  
}  
  
#[cfg(test)]  
mod tests {  
    use super::*;  
  
    #[test]  
    fn argon2_password_hash_works() {  
        let password = String::from("I am a password");  
        let password_hash_output = argon2_password_hash(password.as_bytes());  
        assert!(password_hash_output.is_ok());  
        assert_eq!(password_hash_output.unwrap().len(), 32);  
    }  
}

d. aes-crypto模块

定义两个常量:aes-key长度和aes-nonce长度

以及aes-key字节数组的别名类型PalAesKey

aes_crypto.rs

rust 复制代码
use aes_gcm::{  
    aead::{AeadCore, KeyInit, OsRng},  
    Aes256Gcm,  
};  
use aes_gcm::aead::Aead;  
use aes_gcm::aead::generic_array::GenericArray;  
use anyhow::Result;  
  
  
const AES_KEY_LEN_32: usize = 32;  
const AES_NONCE_LEN_12 : usize = 12;  
  
#[derive(Clone)]  
pub struct PalAesKey(pub [u8; AES_KEY_LEN_32]);  
  
impl PalAesKey {  
    pub fn as_bytes(&self) -> Vec<u8> {  
        self.0.to_vec()  
    }  
}

aes生成随机key和加解密:aes-gcm库内置生成随机密钥key,非用户自定义密码场景会用到。

加解密是典型的对称加密,相同的密钥key,把明文变成密文,密文变成明文,接收字节数组参数,返回字节数组。

aes_crypto.rs

ini 复制代码
pub fn generate_pal_aes_key() -> PalAesKey {  
    let key = Aes256Gcm::generate_key(&mut OsRng);  
    PalAesKey(key.into())  
}  
  
pub fn pal_aes_encrypt(pal_aes_key_bytes: &[u8], plain_bytes: &[u8]) -> Result<Vec<u8>>{  
    let cipher = Aes256Gcm::new(GenericArray::from_slice(&pal_aes_key_bytes));  
    let nonce = Aes256Gcm::generate_nonce(&mut OsRng);  
    let mut encrypted_bytes = cipher.encrypt(&nonce, plain_bytes).unwrap();  
    encrypted_bytes.extend_from_slice(&nonce);  
    Ok(encrypted_bytes)  
}  
  
pub fn pal_aes_decrypt(
  pal_aes_key_bytes: &[u8], 
  encrypted_bytes: &[u8], 
  nonce_len: Option<usize>) -> Result<Vec<u8>> {  
    let cipher = Aes256Gcm::new(GenericArray::from_slice(&pal_aes_key_bytes));  
    let nonce_len = nonce_len.unwrap_or(AES_NONCE_LEN_12);  
    let offset = encrypted_bytes.len() - nonce_len;  
    let nonce = encrypted_bytes[offset..].to_vec();  
    let buffer = encrypted_bytes[..offset].to_vec();  
    let plain_bytes = cipher.decrypt(GenericArray::from_slice(&nonce), buffer.as_ref()).unwrap();  
    Ok(plain_bytes)  
}  
  
#[cfg(test)]  
mod tests {  
    use super::*;  
  
    #[test]  
    fn pal_aes_enc_dec_works(){  
        let key = generate_pal_aes_key();  
        let plain_bytes = b"Hello, Pal!";  
        let encrypted_bytes = pal_aes_encrypt(key.as_bytes().as_slice(), plain_bytes).unwrap();  
        let decrypted_bytes = pal_aes_decrypt(
            key.as_bytes().as_slice(), 
            &encrypted_bytes, None).unwrap();  
        assert_eq!(plain_bytes, decrypted_bytes.as_slice());  
    }  
  
}

e. crypto-box-crypto模块

Vec转换定长Array的工具函数

utils.rs

rust 复制代码
pub fn vec2array<T, const N: usize>(v: Vec<T>) -> [T; N] {  
    v.try_into()  
        .unwrap_or_else(|v: Vec<T>| panic!("Expected a Vec of length {} but it was {}", N, v.len()))  
}

定义一个常量:nonce长度

以及封装了密钥对字节数组的类型PalCryptoKeyPair

PalCryptoKeyPair里的方法用于在密钥不同使用场景下对象的互相转换

crypto_box_crypto.rs

rust 复制代码
use crypto_box::{aead::{Aead, AeadCore, OsRng}, KEY_SIZE};  
use crypto_box::aead::generic_array::GenericArray;  
use anyhow::Result;  
use ed25519_dalek::{Signer, Verifier};  
use crate::utils::vec2array;  
  
pub const NONCE_LEN: usize = 24;  
  
#[derive(Clone)]  
pub struct PalCryptoKeyPair{  
    pub secret_key_bytes: [u8; KEY_SIZE],  
    pub public_key_bytes:  [u8; KEY_SIZE],  
}  
  
impl PalCryptoKeyPair {  
    pub fn secret_key(&self) -> crypto_box::SecretKey{  
        let signing_key = ed25519_dalek::SigningKey::from_bytes(
            &ed25519_dalek::SecretKey::from(self.secret_key_bytes));  
        crypto_box::SecretKey::from(signing_key.to_scalar())  
    }  
  
    pub fn make_secret_key(secret_key_bytes: [u8; KEY_SIZE]) -> crypto_box::SecretKey {  
        let signing_key = Self::make_signing_key(secret_key_bytes.as_slice());  
        crypto_box::SecretKey::from(signing_key.to_scalar())  
    }  
  
    pub fn public_key(&self) -> crypto_box::PublicKey{  
        crypto_box::PublicKey::from(
            ed25519_dalek::VerifyingKey::from_bytes(
                &self.public_key_bytes).unwrap().to_montgomery())  
    }  
  
    pub fn make_public_key(public_key_bytes: [u8; KEY_SIZE]) -> crypto_box::PublicKey {  
        crypto_box::PublicKey::from(
            Self::make_verifying_key(
                public_key_bytes.as_slice()).to_montgomery())  
    }  
  
    pub fn make_signing_key(secret_key_bytes: &[u8]) -> ed25519_dalek::SigningKey {  
        ed25519_dalek::SigningKey::from_bytes(  
            &ed25519_dalek::SecretKey::from(  
                vec2array(secret_key_bytes.to_vec())  
            )  
        )  
    }  
  
    pub fn make_verifying_key(public_key_bytes: &[u8]) -> ed25519_dalek::VerifyingKey {  
        ed25519_dalek::VerifyingKey::from_bytes(
            &vec2array(public_key_bytes.to_vec())).unwrap()  
    }  
  
    pub fn make_cb_box(secret_key_bytes:&[u8], public_key_bytes: &[u8]) -> crypto_box::ChaChaBox {  
        crypto_box::ChaChaBox::new(  
            &PalCryptoKeyPair::make_public_key(  
                vec2array(public_key_bytes.to_vec())),  
            &PalCryptoKeyPair::make_secret_key(  
                vec2array(secret_key_bytes.to_vec())  
            ),  
        )  
    }  
}

crypto_box生成密钥对和加解密函数 此加密方式除了需要对应双方公私钥,还需要约定nonce的长度,用于从密文中分离出加密数据和nonce信息,再进行解密。

crypto_box_crypto.rs

ini 复制代码
pub fn generate_pal_key_pair() -> PalCryptoKeyPair {  
  
    let signing_key = ed25519_dalek::SigningKey::generate(&mut OsRng);  
    PalCryptoKeyPair{  
        secret_key_bytes: signing_key.to_bytes(),  
        public_key_bytes: signing_key.verifying_key().to_bytes(),  
    }  
}  
  
pub fn pal_cb_encrypt(
  public_key_bytes: &[u8], 
  secret_key_bytes: &[u8], 
  plain_bytes: &[u8]) -> Result<Vec<u8>>{  
    let encrypt_box = PalCryptoKeyPair::make_cb_box(  
        secret_key_bytes,  
        public_key_bytes,  
    );  
  
    let nonce = crypto_box::ChaChaBox::generate_nonce(&mut OsRng);  
    let mut cipher_data = encrypt_box.encrypt(&nonce, plain_bytes).unwrap();  
    cipher_data.extend_from_slice(&nonce);  
    Ok(cipher_data)  
}  
  
pub fn pal_cb_decrypt(
  public_key_bytes: &[u8], 
  secret_key_bytes: &[u8], 
  ciphertext: &[u8], 
  nonce_len: Option<usize>) -> Result<Vec<u8>>{  
    let nonce_len = nonce_len.unwrap_or(NONCE_LEN);  
    let offset = ciphertext.len() - nonce_len;  
    let decrypt_box = PalCryptoKeyPair::make_cb_box(  
        secret_key_bytes,  
        public_key_bytes,  
    );  
    let nonce = ciphertext[offset..].to_vec();  
    let payload_data = ciphertext[..offset].to_vec();  
    let plain_bytes = decrypt_box.decrypt(
        GenericArray::from_slice(&nonce), payload_data.as_slice()).unwrap();  
    Ok(plain_bytes)  
}  
  
pub fn pal_cb_sign(secret_key_bytes: &[u8], msg: &[u8]) -> Result<Vec<u8>>{  
    let signing_key = PalCryptoKeyPair::make_signing_key(secret_key_bytes);  
    let sign =signing_key.sign(msg);  
    Ok(sign.to_bytes().to_vec())  
}  
  
pub fn pal_cb_verify_sign(public_key_bytes: &[u8], msg: &[u8], sign: &[u8]) -> Result<bool>{  
    let verifying_key = PalCryptoKeyPair::make_verifying_key(public_key_bytes);  
    let ok = verifying_key.verify(msg, &ed25519_dalek::Signature::from_slice(sign)?).is_ok();  
    Ok(ok)  
}  
  
#[cfg(test)]  
mod tests {  
    use super::*;  
  
    #[test]  
    fn enc_dec_self_works() {  
        let key_pair = generate_pal_key_pair();  
        let plain_bytes = b"I am a plain msg.";  
        let cipher_bytes = pal_cb_encrypt(
            key_pair.public_key_bytes.as_slice(), 
            key_pair.secret_key_bytes.as_slice(), plain_bytes).unwrap();  
        let decrypted_bytes = pal_cb_decrypt(
            key_pair.public_key_bytes.as_slice(), 
            key_pair.secret_key_bytes.as_slice(), &cipher_bytes, None).unwrap();  
        assert_eq!(plain_bytes, decrypted_bytes.as_slice());  
    }  
  
    #[test]  
    fn enc_dec_each_works(){  
        let key_pair_a = generate_pal_key_pair();  
        let key_pair_b = generate_pal_key_pair();  
  
        let a_say = b"Hi, I am a.";  
        let b_say = b"Hi, I am B.";  
  
        let a_say_encrypted = pal_cb_encrypt(
            key_pair_b.public_key_bytes.as_slice(), 
            key_pair_a.secret_key_bytes.as_slice(), a_say).unwrap();  
        let a_say_decrypted = pal_cb_decrypt(
            key_pair_a.public_key_bytes.as_slice(), 
            key_pair_b.secret_key_bytes.as_slice(), 
            a_say_encrypted.as_slice(), None).unwrap();  
        assert_eq!(a_say, a_say_decrypted.as_slice());  
  
        let b_say_encrypted = pal_cb_encrypt(
            key_pair_a.public_key_bytes.as_slice(), 
            key_pair_b.secret_key_bytes.as_slice(), b_say).unwrap();  
        let b_say_decrypted = pal_cb_decrypt(
            key_pair_b.public_key_bytes.as_slice(), 
            key_pair_a.secret_key_bytes.as_slice(), 
            b_say_encrypted.as_slice(), None).unwrap();  
        assert_eq!(b_say, b_say_decrypted.as_slice());  
    }  
  
    #[test]  
    fn sign_verify_works(){  
        let key_pair = generate_pal_key_pair();  
        let msg = b"Hi, this is my signature.";  
        let sign = pal_cb_sign(key_pair.secret_key_bytes.as_slice(), msg).unwrap();  
        assert!(pal_cb_verify_sign(key_pair.public_key_bytes.as_slice(), msg, &sign).unwrap());  
    }  
}

总结

通过本文的介绍和实践,相信大家已经清楚地认识到 ED25519 相比 RSA 的优势,以及如何使用 Rust 的crypto_box库来应用 ED25519 算法。

在当今快速发展的数字时代,为了确保数据的安全和系统的高效运行,是时候考虑将 ED25519 引入到我们的项目中了。

当然,密码学领域的探索永无止境,未来或许还会有更先进的算法出现。但就目前而言,ED25519 无疑是一个非常优秀的选择。

赶紧动手,在你的项目中试试用 ED25519 替代 RSA 吧!

本专栏专注Rust实践,欢迎关注和交流。

附:封装库源码:bambipig/palcrypto-rs: A crypto lib.

相关推荐
BullSmall2 小时前
第三方软件安全-Dependency‑Track Docker 部署
安全·docker·容器
mldong2 小时前
流程实例如何沿着箭头走到结束:Rust 工作流引擎的执行内核
架构·rust
AImatters2 小时前
国产算力底座,支撑油气储运数字化迈过关键分水岭
安全·cpu·算力·海光·油气储运
叠叠乐5 小时前
中国移动家庭云电脑window关闭所有安全脚本
安全
Jmyd01236 小时前
实训室的 3D 模型涉及肖像文物,数据安全与合规怎么做?
安全·3d·数据安全·虚拟实训
芯盾时代6 小时前
《金融业网络安全管理办法(征求意见稿)》全条款深度拆解(三)
网络·安全·网络安全
敢敢是只喵i6 小时前
一个本地 AI Agent 要操作多个门店或 SaaS 账号,应该怎样安全切换身份?
人工智能·安全·ai·系统架构·业界资讯
Acrel12347 小时前
DC800V 大规模落地,算力机房安全该如何保障
安全
其实防守也摸鱼8 小时前
权限提升与横向移动:从内网渗透到域控的完整技术图谱
运维·服务器·数据库·安全·github·copilot·渗透
深圳海导科技8 小时前
资产防盗追踪系统:物联网高价值资产安全管理|海导科技navynav
科技·物联网·安全