From 1d74eab86a0a3a8aafb547448b7fbe4028e12168 Mon Sep 17 00:00:00 2001 From: Dan Seminara Date: Wed, 18 Mar 2026 23:35:14 -0400 Subject: [PATCH] Use `ring` instead of `chacha20poly1305` crate --- Cargo.lock | 71 ++++++++++++++++-------------------- Cargo.toml | 2 +- age-core/Cargo.toml | 2 +- age-core/src/primitives.rs | 42 +++++++++++++++------ age/Cargo.toml | 2 +- age/src/error.rs | 4 +- age/src/primitives/stream.rs | 44 +++++++++++++--------- 7 files changed, 92 insertions(+), 75 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 37a3e255..92a61df0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -69,7 +69,6 @@ dependencies = [ "bcrypt-pbkdf", "bech32", "cbc", - "chacha20poly1305", "cipher", "console", "cookie-factory", @@ -93,6 +92,7 @@ dependencies = [ "pprof", "proptest", "rand", + "ring", "rpassword", "rsa", "rust-embed", @@ -113,12 +113,12 @@ name = "age-core" version = "0.11.0" dependencies = [ "base64", - "chacha20poly1305", "cookie-factory", "hkdf", "io_tee", "nom", "rand", + "ring", "secrecy", "sha2", "tempfile", @@ -408,10 +408,11 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.33" +version = "1.2.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ee0f8803222ba5a7e2777dd72ca451868909b1ac410621b676adf07280e9b5f" +checksum = "9f50d563227a1c37cc0a263f64eca3334388c01c5e4c4861a9def205c614383c" dependencies = [ + "find-msvc-tools", "jobserver", "libc", "shlex", @@ -423,30 +424,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" -[[package]] -name = "chacha20" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures", -] - -[[package]] -name = "chacha20poly1305" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" -dependencies = [ - "aead", - "chacha20", - "cipher", - "poly1305", - "zeroize", -] - [[package]] name = "chrono" version = "0.4.39" @@ -496,7 +473,6 @@ checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common", "inout", - "zeroize", ] [[package]] @@ -895,6 +871,12 @@ dependencies = [ "toml", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" + [[package]] name = "findshlibs" version = "0.10.2" @@ -1845,17 +1827,6 @@ dependencies = [ "plotters-backend", ] -[[package]] -name = "poly1305" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" -dependencies = [ - "cpufeatures", - "opaque-debug", - "universal-hash", -] - [[package]] name = "polyval" version = "0.6.2" @@ -2114,6 +2085,20 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "roff" version = "0.2.1" @@ -2815,6 +2800,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "utf8parse" version = "0.2.2" diff --git a/Cargo.toml b/Cargo.toml index fde3ada2..cca21e2d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ age-core = { version = "0.11.0", path = "age-core" } base64 = "0.21" # - ChaCha20-Poly1305 from RFC 7539 -chacha20poly1305 = { version = "0.10", default-features = false, features = ["alloc"] } +ring = "0.17.14" # - X25519 from RFC 7748 x25519-dalek = { version = "2", features = ["static_secrets"] } diff --git a/age-core/Cargo.toml b/age-core/Cargo.toml index 131a54dd..d7ecdaf5 100644 --- a/age-core/Cargo.toml +++ b/age-core/Cargo.toml @@ -19,7 +19,7 @@ maintenance = { status = "experimental" } [dependencies] # Dependencies exposed in a public API: # (Breaking upgrades to these require a breaking upgrade to this crate.) -chacha20poly1305.workspace = true +ring.workspace = true cookie-factory.workspace = true io_tee = "0.1.1" nom.workspace = true diff --git a/age-core/src/primitives.rs b/age-core/src/primitives.rs index 75571873..0e1035df 100644 --- a/age-core/src/primitives.rs +++ b/age-core/src/primitives.rs @@ -1,10 +1,10 @@ //! Primitive cryptographic operations used across various `age` components. -use chacha20poly1305::{ - aead::{self, generic_array::typenum::Unsigned, Aead, AeadCore, KeyInit}, - ChaCha20Poly1305, -}; use hkdf::Hkdf; +use ring::{ + aead::{Aad, LessSafeKey, Nonce, UnboundKey, CHACHA20_POLY1305}, + error, +}; use sha2::Sha256; /// `encrypt[key](plaintext)` - encrypts a message with a one-time key. @@ -13,9 +13,18 @@ use sha2::Sha256; /// /// [RFC 7539]: https://tools.ietf.org/html/rfc7539 pub fn aead_encrypt(key: &[u8; 32], plaintext: &[u8]) -> Vec { - let c = ChaCha20Poly1305::new(key.into()); - c.encrypt(&[0; 12].into(), plaintext) - .expect("we won't overflow the ChaCha20 block counter") + let k = LessSafeKey::new( + UnboundKey::new(&CHACHA20_POLY1305, key).expect("byte length of key will match expected"), + ); + let mut buffer = Vec::with_capacity(plaintext.len() + CHACHA20_POLY1305.tag_len()); + buffer.extend_from_slice(plaintext); + k.seal_in_place_append_tag( + Nonce::assume_unique_for_key([0; 12]), + Aad::empty(), + &mut buffer, + ) + .expect("encryption won't fail"); + buffer } /// `decrypt[key](ciphertext)` - decrypts a message of an expected fixed size. @@ -31,13 +40,22 @@ pub fn aead_decrypt( key: &[u8; 32], size: usize, ciphertext: &[u8], -) -> Result, aead::Error> { - if ciphertext.len() != size + ::TagSize::to_usize() { - return Err(aead::Error); +) -> Result, error::Unspecified> { + if ciphertext.len() != size + CHACHA20_POLY1305.tag_len() { + return Err(error::Unspecified); } - let c = ChaCha20Poly1305::new(key.into()); - c.decrypt(&[0; 12].into(), ciphertext) + let k = LessSafeKey::new( + UnboundKey::new(&CHACHA20_POLY1305, key).expect("byte length of key will match expected"), + ); + let mut buffer = Vec::from(ciphertext); + k.open_in_place( + Nonce::assume_unique_for_key([0; 12]), + Aad::empty(), + &mut buffer, + )?; + buffer.truncate(buffer.len() - CHACHA20_POLY1305.tag_len()); + Ok(buffer) } /// `HKDF[salt, label](key, 32)` diff --git a/age/Cargo.toml b/age/Cargo.toml index 994ca3f5..5e344e99 100644 --- a/age/Cargo.toml +++ b/age/Cargo.toml @@ -20,7 +20,7 @@ age-core.workspace = true # Dependencies exposed in a public API: # (Breaking upgrades to these require a breaking upgrade to this crate.) base64.workspace = true -chacha20poly1305.workspace = true +ring.workspace = true hmac.workspace = true i18n-embed.workspace = true rand.workspace = true diff --git a/age/src/error.rs b/age/src/error.rs index 5505d4dd..5d19ca3c 100644 --- a/age/src/error.rs +++ b/age/src/error.rs @@ -413,8 +413,8 @@ impl fmt::Display for DecryptError { } } -impl From for DecryptError { - fn from(_: chacha20poly1305::aead::Error) -> Self { +impl From for DecryptError { + fn from(_: ring::error::Unspecified) -> Self { DecryptError::DecryptionFailed } } diff --git a/age/src/primitives/stream.rs b/age/src/primitives/stream.rs index 652e79ce..9aa3b7e6 100644 --- a/age/src/primitives/stream.rs +++ b/age/src/primitives/stream.rs @@ -1,11 +1,8 @@ //! I/O helper structs for age file encryption and decryption. use age_core::secrecy::{ExposeSecret, SecretSlice}; -use chacha20poly1305::{ - aead::{generic_array::GenericArray, Aead, KeyInit, KeySizeUser}, - ChaCha20Poly1305, -}; use pin_project::pin_project; +use ring::aead::{self, Aad, LessSafeKey, UnboundKey, CHACHA20_POLY1305}; use std::cmp; use std::io::{self, Read, Seek, SeekFrom, Write}; use zeroize::Zeroize; @@ -20,12 +17,11 @@ use futures::{ use std::pin::Pin; const CHUNK_SIZE: usize = 64 * 1024; +const KEY_SIZE: usize = 32; const TAG_SIZE: usize = 16; const ENCRYPTED_CHUNK_SIZE: usize = CHUNK_SIZE + TAG_SIZE; -pub(crate) struct PayloadKey( - pub(crate) GenericArray::KeySize>, -); +pub(crate) struct PayloadKey(pub(crate) [u8; KEY_SIZE]); impl Drop for PayloadKey { fn drop(&mut self) { @@ -89,14 +85,17 @@ struct EncryptedChunk { /// /// [STREAM]: https://eprint.iacr.org/2015/189.pdf pub(crate) struct Stream { - aead: ChaCha20Poly1305, + key: LessSafeKey, nonce: Nonce, } impl Stream { fn new(key: PayloadKey) -> Self { Stream { - aead: ChaCha20Poly1305::new(&key.0), + key: LessSafeKey::new( + UnboundKey::new(&CHACHA20_POLY1305, &key.0) + .expect("byte length of key will match expected"), + ), nonce: Nonce::default(), } } @@ -185,10 +184,15 @@ impl Stream { io::Error::new(io::ErrorKind::WriteZero, "last chunk has been processed") })?; - let encrypted = self - .aead - .encrypt(&self.nonce.to_bytes().into(), chunk) - .expect("we will never hit chacha20::MAX_BLOCKS because of the chunk size"); + let mut encrypted = Vec::with_capacity(chunk.len() + CHACHA20_POLY1305.tag_len()); + encrypted.extend_from_slice(chunk); + self.key + .seal_in_place_append_tag( + aead::Nonce::assume_unique_for_key(self.nonce.to_bytes()), + Aad::empty(), + &mut encrypted, + ) + .expect("encryption won't fail"); self.nonce.increment_counter(); Ok(encrypted) @@ -201,14 +205,18 @@ impl Stream { io::Error::new(io::ErrorKind::InvalidData, "last chunk has been processed") })?; - let decrypted = self - .aead - .decrypt(&self.nonce.to_bytes().into(), chunk) - .map(SecretSlice::from) + let mut decrypted = Vec::from(chunk); + self.key + .open_in_place( + aead::Nonce::assume_unique_for_key(self.nonce.to_bytes()), + Aad::empty(), + &mut decrypted, + ) .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "decryption error"))?; + decrypted.truncate(decrypted.len() - CHACHA20_POLY1305.tag_len()); self.nonce.increment_counter(); - Ok(decrypted) + Ok(SecretSlice::from(decrypted)) } fn is_complete(&self) -> bool {