背景
在密码学的世界里,算法的更新迭代就像一场永不停歇的竞赛。
曾经,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_box
和aes-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实践,欢迎关注和交流。