是时候用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.

相关推荐
☞无能盖世♛逞何英雄☜1 小时前
SSRF漏洞
安全·web安全
Humbunklung7 小时前
Rust 控制流
开发语言·算法·rust
芯盾时代10 小时前
AI在网络安全领域的应用现状和实践
人工智能·安全·web安全·网络安全
银空飞羽11 小时前
再学学MCP间接提示词注入
安全·mcp·trae
安全系统学习15 小时前
【网络安全】漏洞分析:阿帕奇漏洞学习
java·网络·安全·web安全·系统安全
mooyuan天天19 小时前
pikachu靶场通关笔记14 XSS关卡10-XSS之js输出(五种方法渗透)
安全·web安全·xss·pikachu靶场·xss漏洞
UestcXiye21 小时前
Rust 学习笔记:Box<T>
rust
conkl21 小时前
Kali Linux 安全工具解析
linux·网络·安全·kali
天翼云开发者社区1 天前
网络安全自动化:最需要自动化的 12 个关键功能
安全
小格子衬衫1 天前
系统巡检常见工作
网络·安全·web安全