From 52add5fac44f7685f1b927103b8417437fcb6c27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20F=C3=A4rnstrand?= Date: Thu, 16 Apr 2026 09:46:30 +0000 Subject: [PATCH] Replace ring-compat crypto backend with aws-lc-rs Swap the optional `ring`/`ring-compat` dependency for `aws-lc-rs`, which wraps the actively maintained AWS-LC C library and delivers comparable (or better) AEAD/HKDF throughput without pulling in `ring`. The cargo feature is renamed from `ring` to `faster-crypto` so downstream users express intent (a faster crypto backend) rather than the specific implementation. Platforms not covered by aws-lc-rs (e.g. WASM, exotic targets) must leave the feature off and fall back to the pure-Rust path. The AEAD code under the feature gate now calls aws-lc-rs natively via `LessSafeKey` / `UnboundKey` / `Nonce` / `Aad` / `seal_in_place_separate_tag` / `open_in_place`. The HKDF-SHA1 path switches from `ring_compat::ring::hkdf` to `aws_lc_rs::hkdf`, which exposes the same `Salt` / `Prk` / `Okm` / `KeyType` surface, so that block is a near-verbatim import swap. The pure-Rust `else` branches are untouched apart from moving the struct + impl blocks inside the `cfg_if!` so each backend is self-contained. MSRV is bumped from 1.63 to 1.71 to match aws-lc-rs 1.16. The Windows CI job also needs NASM preinstalled because aws-lc-sys assembles its x86_64 kernels with it; the clippy job already did this, so the build and test job gets the same `ilammy/setup-nasm@v1` step. --- .github/workflows/build-and-test.yml | 2 + Cargo.toml | 9 +- src/v1/aeadcipher/aes_gcm.rs | 207 +++++++++++++------------ src/v1/aeadcipher/chacha20_poly1305.rs | 117 +++++++------- src/v1/cipher.rs | 4 +- 5 files changed, 174 insertions(+), 165 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 99722c3..d896a07 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -22,6 +22,8 @@ jobs: steps: - uses: actions/checkout@v5 - uses: Swatinem/rust-cache@v2 + - if: ${{ runner.os == 'Windows' }} + uses: ilammy/setup-nasm@v1 - name: Install Rust run: | rustup set profile minimal diff --git a/Cargo.toml b/Cargo.toml index c32f3fa..8261b3b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ keywords = ["Cryptography"] description = "Shadowsocks Crypto" repository = "https://github.com/shadowsocks/shadowsocks-crypto" documentation = "https://docs.rs/shadowsocks-crypto" -rust-version = "1.63" +rust-version = "1.71" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] @@ -30,7 +30,10 @@ v1-aead-extra = [ v2 = ["aes", "aes-gcm", "blake3", "chacha20poly1305", "bytes", "cfg-if"] v2-extra = ["v2", "chacha20poly1305/reduced-round"] -ring = ["ring-compat"] +# Swap the pure-Rust AEAD/HKDF implementations for the AWS-LC-backed `aws-lc-rs` +# crate for higher throughput. Platforms not supported by aws-lc-rs (e.g. WASM, +# exotic targets) must leave this feature off and use the pure-Rust fallback. +faster-crypto = ["dep:aws-lc-rs"] [dependencies] cfg-if = { version = "1.0", optional = true } @@ -39,7 +42,7 @@ aes-gcm = { version = "0.10", optional = true } aes-gcm-siv = { version = "0.11", optional = true } ccm = { version = "0.5", optional = true } chacha20poly1305 = { version = "0.10", optional = true } -ring-compat = { version = "0.8", default-features = false, features = ["aead", "alloc"], optional = true } +aws-lc-rs = { version = "1.16", optional = true } md-5 = { version = "0.10", optional = true } hkdf = { version = "0.12", optional = true } sha1 = { version = "0.10", optional = true } diff --git a/src/v1/aeadcipher/aes_gcm.rs b/src/v1/aeadcipher/aes_gcm.rs index ed013f7..318b53f 100644 --- a/src/v1/aeadcipher/aes_gcm.rs +++ b/src/v1/aeadcipher/aes_gcm.rs @@ -1,39 +1,69 @@ use cfg_if::cfg_if; cfg_if! { - if #[cfg(feature = "ring")] { - use std::convert::{AsMut, AsRef}; + if #[cfg(feature = "faster-crypto")] { + use aws_lc_rs::aead::{Aad, Algorithm, LessSafeKey, Nonce, UnboundKey, AES_128_GCM, AES_256_GCM}; - pub use ring_compat::aead::{Aes128Gcm as CryptoAes128Gcm, Aes256Gcm as CryptoAes256Gcm}; - use ring_compat::{ - aead::{AeadCore, AeadInPlace, Buffer, Error as AeadError, KeySizeUser, KeyInit}, - generic_array::{typenum::Unsigned, GenericArray}, - }; - - type Key = GenericArray::KeySize>; - type Nonce = GenericArray; + struct AeadKey(LessSafeKey); - struct SliceBuffer<'a>(&'a mut [u8]); + impl AeadKey { + fn new(algorithm: &'static Algorithm, key: &[u8]) -> AeadKey { + let unbound = UnboundKey::new(algorithm, key).expect("AEAD key"); + AeadKey(LessSafeKey::new(unbound)) + } - impl AsRef<[u8]> for SliceBuffer<'_> { - fn as_ref(&self) -> &[u8] { - self.0 + fn encrypt(&self, nonce: &[u8], plaintext_in_ciphertext_out: &mut [u8]) { + let nonce = Nonce::try_assume_unique_for_key(nonce).expect("AEAD nonce"); + let tag_len = self.0.algorithm().tag_len(); + let (plaintext, out_tag) = + plaintext_in_ciphertext_out.split_at_mut(plaintext_in_ciphertext_out.len() - tag_len); + let tag = self + .0 + .seal_in_place_separate_tag(nonce, Aad::empty(), plaintext) + .expect("AEAD encrypt"); + out_tag.copy_from_slice(tag.as_ref()); } - } - impl AsMut<[u8]> for SliceBuffer<'_> { - fn as_mut(&mut self) -> &mut [u8] { - self.0 + fn decrypt(&self, nonce: &[u8], ciphertext_in_plaintext_out: &mut [u8]) -> bool { + let nonce = Nonce::try_assume_unique_for_key(nonce).expect("AEAD nonce"); + self.0.open_in_place(nonce, Aad::empty(), ciphertext_in_plaintext_out).is_ok() } } - impl Buffer for SliceBuffer<'_> { - fn extend_from_slice(&mut self, _other: &[u8]) -> Result<(), AeadError> { - unimplemented!("not used in decrypt_in_place") - } + macro_rules! aead_cipher { + ($name:ident, $algorithm:ident) => { + pub struct $name(AeadKey); + + impl $name { + pub fn new(key: &[u8]) -> $name { + $name(AeadKey::new(&$algorithm, key)) + } + + pub fn key_size() -> usize { + $algorithm.key_len() + } + + pub fn nonce_size() -> usize { + $algorithm.nonce_len() + } + + pub fn tag_size() -> usize { + $algorithm.tag_len() + } + + pub fn encrypt(&self, nonce: &[u8], plaintext_in_ciphertext_out: &mut [u8]) { + self.0.encrypt(nonce, plaintext_in_ciphertext_out) + } - fn truncate(&mut self, _len: usize) {} + pub fn decrypt(&self, nonce: &[u8], ciphertext_in_plaintext_out: &mut [u8]) -> bool { + self.0.decrypt(nonce, ciphertext_in_plaintext_out) + } + } + }; } + + aead_cipher!(Aes128Gcm, AES_128_GCM); + aead_cipher!(Aes256Gcm, AES_256_GCM); } else { use aes_gcm::{ aead::{generic_array::typenum::Unsigned, AeadCore, AeadInPlace, KeySizeUser, KeyInit}, @@ -41,102 +71,81 @@ cfg_if! { Nonce, Tag, }; - pub use aes_gcm::{Aes128Gcm as CryptoAes128Gcm, Aes256Gcm as CryptoAes256Gcm}; - } -} + use aes_gcm::{Aes128Gcm as CryptoAes128Gcm, Aes256Gcm as CryptoAes256Gcm}; -pub struct Aes128Gcm(Box); - -impl Aes128Gcm { - pub fn new(key: &[u8]) -> Aes128Gcm { - let key = Key::::from_slice(key); - Aes128Gcm(Box::new(CryptoAes128Gcm::new(key))) - } + pub struct Aes128Gcm(Box); - pub fn key_size() -> usize { - ::KeySize::to_usize() - } - - pub fn nonce_size() -> usize { - ::NonceSize::to_usize() - } + impl Aes128Gcm { + pub fn new(key: &[u8]) -> Aes128Gcm { + let key = Key::::from_slice(key); + Aes128Gcm(Box::new(CryptoAes128Gcm::new(key))) + } - pub fn tag_size() -> usize { - ::TagSize::to_usize() - } + pub fn key_size() -> usize { + ::KeySize::to_usize() + } - pub fn encrypt(&self, nonce: &[u8], plaintext_in_ciphertext_out: &mut [u8]) { - let nonce = Nonce::from_slice(nonce); - let (plaintext, out_tag) = - plaintext_in_ciphertext_out.split_at_mut(plaintext_in_ciphertext_out.len() - Self::tag_size()); - let tag = self - .0 - .encrypt_in_place_detached(nonce, &[], plaintext) - .expect("AES_128_GCM encrypt"); - out_tag.copy_from_slice(tag.as_slice()) - } + pub fn nonce_size() -> usize { + ::NonceSize::to_usize() + } - pub fn decrypt(&self, nonce: &[u8], ciphertext_in_plaintext_out: &mut [u8]) -> bool { - let nonce = Nonce::from_slice(nonce); + pub fn tag_size() -> usize { + ::TagSize::to_usize() + } - cfg_if! { - if #[cfg(feature = "ring")] { - // ring-compat marked decrypt_in_place_detached as unimplemented. - // But AES_128_GCM actually expects tag in the back. So it is safe to use `decrypt_in_place`. + pub fn encrypt(&self, nonce: &[u8], plaintext_in_ciphertext_out: &mut [u8]) { + let nonce = Nonce::from_slice(nonce); + let (plaintext, out_tag) = + plaintext_in_ciphertext_out.split_at_mut(plaintext_in_ciphertext_out.len() - Self::tag_size()); + let tag = self + .0 + .encrypt_in_place_detached(nonce, &[], plaintext) + .expect("AES_128_GCM encrypt"); + out_tag.copy_from_slice(tag.as_slice()) + } - let mut buffer = SliceBuffer(ciphertext_in_plaintext_out); - self.0.decrypt_in_place(nonce, &[], &mut buffer).is_ok() - } else { + pub fn decrypt(&self, nonce: &[u8], ciphertext_in_plaintext_out: &mut [u8]) -> bool { + let nonce = Nonce::from_slice(nonce); let (ciphertext, in_tag) = ciphertext_in_plaintext_out.split_at_mut(ciphertext_in_plaintext_out.len() - Self::tag_size()); let in_tag = Tag::from_slice(in_tag); self.0.decrypt_in_place_detached(nonce, &[], ciphertext, in_tag).is_ok() } } - } -} -pub struct Aes256Gcm(Box); - -impl Aes256Gcm { - pub fn new(key: &[u8]) -> Aes256Gcm { - let key = Key::::from_slice(key); - Aes256Gcm(Box::new(CryptoAes256Gcm::new(key))) - } + pub struct Aes256Gcm(Box); - pub fn key_size() -> usize { - ::KeySize::to_usize() - } + impl Aes256Gcm { + pub fn new(key: &[u8]) -> Aes256Gcm { + let key = Key::::from_slice(key); + Aes256Gcm(Box::new(CryptoAes256Gcm::new(key))) + } - pub fn nonce_size() -> usize { - ::NonceSize::to_usize() - } + pub fn key_size() -> usize { + ::KeySize::to_usize() + } - pub fn tag_size() -> usize { - ::TagSize::to_usize() - } + pub fn nonce_size() -> usize { + ::NonceSize::to_usize() + } - pub fn encrypt(&self, nonce: &[u8], plaintext_in_ciphertext_out: &mut [u8]) { - let nonce = Nonce::from_slice(nonce); - let (plaintext, out_tag) = - plaintext_in_ciphertext_out.split_at_mut(plaintext_in_ciphertext_out.len() - Self::tag_size()); - let tag = self - .0 - .encrypt_in_place_detached(nonce, &[], plaintext) - .expect("AES_256_GCM encrypt"); - out_tag.copy_from_slice(tag.as_slice()) - } + pub fn tag_size() -> usize { + ::TagSize::to_usize() + } - pub fn decrypt(&self, nonce: &[u8], ciphertext_in_plaintext_out: &mut [u8]) -> bool { - let nonce = Nonce::from_slice(nonce); - cfg_if! { - if #[cfg(feature = "ring")] { - // ring-compat marked decrypt_in_place_detached as unimplemented. - // But AES_256_GCM actually expects tag in the back. So it is safe to use `decrypt_in_place`. + pub fn encrypt(&self, nonce: &[u8], plaintext_in_ciphertext_out: &mut [u8]) { + let nonce = Nonce::from_slice(nonce); + let (plaintext, out_tag) = + plaintext_in_ciphertext_out.split_at_mut(plaintext_in_ciphertext_out.len() - Self::tag_size()); + let tag = self + .0 + .encrypt_in_place_detached(nonce, &[], plaintext) + .expect("AES_256_GCM encrypt"); + out_tag.copy_from_slice(tag.as_slice()) + } - let mut buffer = SliceBuffer(ciphertext_in_plaintext_out); - self.0.decrypt_in_place(nonce, &[], &mut buffer).is_ok() - } else { + pub fn decrypt(&self, nonce: &[u8], ciphertext_in_plaintext_out: &mut [u8]) -> bool { + let nonce = Nonce::from_slice(nonce); let (ciphertext, in_tag) = ciphertext_in_plaintext_out.split_at_mut(ciphertext_in_plaintext_out.len() - Self::tag_size()); let in_tag = Tag::from_slice(in_tag); diff --git a/src/v1/aeadcipher/chacha20_poly1305.rs b/src/v1/aeadcipher/chacha20_poly1305.rs index 4464532..79fbf29 100644 --- a/src/v1/aeadcipher/chacha20_poly1305.rs +++ b/src/v1/aeadcipher/chacha20_poly1305.rs @@ -1,92 +1,87 @@ use cfg_if::cfg_if; cfg_if! { - if #[cfg(feature = "ring")] { - use std::convert::{AsMut, AsRef}; + if #[cfg(feature = "faster-crypto")] { + use aws_lc_rs::aead::{Aad, LessSafeKey, Nonce, UnboundKey, CHACHA20_POLY1305}; - pub use ring_compat::aead::{ChaCha20Poly1305 as CryptoChaCha20Poly1305}; - use ring_compat::{ - aead::{AeadCore, AeadInPlace, Buffer, Error as AeadError, KeySizeUser, KeyInit}, - generic_array::{typenum::Unsigned, GenericArray}, - }; + pub struct ChaCha20Poly1305(LessSafeKey); - type Key = GenericArray; - type Nonce = GenericArray; + impl ChaCha20Poly1305 { + pub fn new(key: &[u8]) -> ChaCha20Poly1305 { + let unbound = UnboundKey::new(&CHACHA20_POLY1305, key).expect("CHACHA20_POLY1305 key"); + ChaCha20Poly1305(LessSafeKey::new(unbound)) + } - struct SliceBuffer<'a>(&'a mut [u8]); + pub fn key_size() -> usize { + CHACHA20_POLY1305.key_len() + } - impl AsRef<[u8]> for SliceBuffer<'_> { - fn as_ref(&self) -> &[u8] { - self.0 + pub fn nonce_size() -> usize { + CHACHA20_POLY1305.nonce_len() } - } - impl AsMut<[u8]> for SliceBuffer<'_> { - fn as_mut(&mut self) -> &mut [u8] { - self.0 + pub fn tag_size() -> usize { + CHACHA20_POLY1305.tag_len() } - } - impl Buffer for SliceBuffer<'_> { - fn extend_from_slice(&mut self, _other: &[u8]) -> Result<(), AeadError> { - unimplemented!("not used in decrypt_in_place") + pub fn encrypt(&self, nonce: &[u8], plaintext_in_ciphertext_out: &mut [u8]) { + let nonce = Nonce::try_assume_unique_for_key(nonce).expect("CHACHA20_POLY1305 nonce"); + let (plaintext, out_tag) = + plaintext_in_ciphertext_out.split_at_mut(plaintext_in_ciphertext_out.len() - Self::tag_size()); + let tag = self + .0 + .seal_in_place_separate_tag(nonce, Aad::empty(), plaintext) + .expect("CHACHA20_POLY1305 encrypt"); + out_tag.copy_from_slice(tag.as_ref()); } - fn truncate(&mut self, _len: usize) {} + pub fn decrypt(&self, nonce: &[u8], ciphertext_in_plaintext_out: &mut [u8]) -> bool { + let nonce = Nonce::try_assume_unique_for_key(nonce).expect("CHACHA20_POLY1305 nonce"); + self.0.open_in_place(nonce, Aad::empty(), ciphertext_in_plaintext_out).is_ok() + } } } else { - pub use chacha20poly1305::ChaCha20Poly1305 as CryptoChaCha20Poly1305; + use chacha20poly1305::ChaCha20Poly1305 as CryptoChaCha20Poly1305; use chacha20poly1305::{ aead::{generic_array::typenum::Unsigned, AeadCore, AeadInPlace, KeySizeUser, KeyInit}, Key, Nonce, Tag, }; - } -} - -pub struct ChaCha20Poly1305(CryptoChaCha20Poly1305); - -impl ChaCha20Poly1305 { - pub fn new(key: &[u8]) -> ChaCha20Poly1305 { - let key = Key::from_slice(key); - ChaCha20Poly1305(CryptoChaCha20Poly1305::new(key)) - } - pub fn key_size() -> usize { - ::KeySize::to_usize() - } + pub struct ChaCha20Poly1305(CryptoChaCha20Poly1305); - pub fn nonce_size() -> usize { - ::NonceSize::to_usize() - } + impl ChaCha20Poly1305 { + pub fn new(key: &[u8]) -> ChaCha20Poly1305 { + let key = Key::from_slice(key); + ChaCha20Poly1305(CryptoChaCha20Poly1305::new(key)) + } - pub fn tag_size() -> usize { - ::TagSize::to_usize() - } + pub fn key_size() -> usize { + ::KeySize::to_usize() + } - pub fn encrypt(&self, nonce: &[u8], plaintext_in_ciphertext_out: &mut [u8]) { - let nonce = Nonce::from_slice(nonce); - let (plaintext, out_tag) = - plaintext_in_ciphertext_out.split_at_mut(plaintext_in_ciphertext_out.len() - Self::tag_size()); - let tag = self - .0 - .encrypt_in_place_detached(nonce, &[], plaintext) - .expect("CHACHA20_POLY1305 encrypt"); - out_tag.copy_from_slice(tag.as_slice()) - } + pub fn nonce_size() -> usize { + ::NonceSize::to_usize() + } - pub fn decrypt(&self, nonce: &[u8], ciphertext_in_plaintext_out: &mut [u8]) -> bool { - let nonce = Nonce::from_slice(nonce); + pub fn tag_size() -> usize { + ::TagSize::to_usize() + } - cfg_if! { - if #[cfg(feature = "ring")] { - // ring-compat marked decrypt_in_place_detached as unimplemented. - // But CHACHA20_POLY1305 actually expects tag in the back. So it is safe to use `decrypt_in_place`. + pub fn encrypt(&self, nonce: &[u8], plaintext_in_ciphertext_out: &mut [u8]) { + let nonce = Nonce::from_slice(nonce); + let (plaintext, out_tag) = + plaintext_in_ciphertext_out.split_at_mut(plaintext_in_ciphertext_out.len() - Self::tag_size()); + let tag = self + .0 + .encrypt_in_place_detached(nonce, &[], plaintext) + .expect("CHACHA20_POLY1305 encrypt"); + out_tag.copy_from_slice(tag.as_slice()) + } - let mut buffer = SliceBuffer(ciphertext_in_plaintext_out); - self.0.decrypt_in_place(nonce, &[], &mut buffer).is_ok() - } else { + pub fn decrypt(&self, nonce: &[u8], ciphertext_in_plaintext_out: &mut [u8]) -> bool { + let nonce = Nonce::from_slice(nonce); let (ciphertext, in_tag) = ciphertext_in_plaintext_out.split_at_mut(ciphertext_in_plaintext_out.len() - Self::tag_size()); let in_tag = Tag::from_slice(in_tag); diff --git a/src/v1/cipher.rs b/src/v1/cipher.rs index b13ad1a..e9f9145 100644 --- a/src/v1/cipher.rs +++ b/src/v1/cipher.rs @@ -96,8 +96,8 @@ impl Cipher { let mut okm = [0u8; MAX_KEY_LEN]; cfg_if! { - if #[cfg(feature = "ring")] { - use ring_compat::ring::hkdf::{Salt, HKDF_SHA1_FOR_LEGACY_USE_ONLY, KeyType}; + if #[cfg(feature = "faster-crypto")] { + use aws_lc_rs::hkdf::{Salt, HKDF_SHA1_FOR_LEGACY_USE_ONLY, KeyType}; struct CryptoKeyType(usize);