From 1fff552bda55c4310bc53886c36d2377982270dd Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Sun, 7 Dec 2025 13:52:09 +0000 Subject: [PATCH 01/20] age: Add a test comparing sync and async armor writing --- age/src/primitives/armor.rs | 69 +++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/age/src/primitives/armor.rs b/age/src/primitives/armor.rs index c1ba9681..7dac376a 100644 --- a/age/src/primitives/armor.rs +++ b/age/src/primitives/armor.rs @@ -1523,4 +1523,73 @@ mod tests { r.read_exact(&mut buf).unwrap(); assert_eq!(&buf[..], &data[data.len() - 1337..data.len() - 1237]); } + + #[cfg(feature = "async")] + #[test] + fn armored_async_cross_check() { + let data = + vec![42; (super::BASE64_CHUNK_SIZE_BYTES * 2) + (super::ARMORED_BYTES_PER_LINE * 10)]; + + let mut encoded_sync = vec![]; + { + let mut out = + ArmoredWriter::wrap_output(&mut encoded_sync, Format::AsciiArmor).unwrap(); + out.write_all(&data).unwrap(); + out.finish().unwrap(); + } + + let mut encoded_async = vec![]; + { + let w = ArmoredWriter::wrap_async_output(&mut encoded_async, Format::AsciiArmor); + pin_mut!(w); + + let mut cx = noop_context(); + + let mut tmp = &data[..]; + loop { + match w.as_mut().poll_write(&mut cx, tmp) { + Poll::Ready(Ok(0)) => break, + Poll::Ready(Ok(written)) => tmp = &tmp[written..], + Poll::Ready(Err(e)) => panic!("Unexpected error: {}", e), + Poll::Pending => panic!("Unexpected Pending"), + } + } + loop { + match w.as_mut().poll_close(&mut cx) { + Poll::Ready(Ok(())) => break, + Poll::Ready(Err(e)) => panic!("Unexpected error: {}", e), + Poll::Pending => panic!("Unexpected Pending"), + } + } + } + + assert_eq!(encoded_sync, encoded_async); + + let mut buf_sync = vec![]; + { + let mut input = ArmoredReader::new(&encoded_sync[..]); + input.read_to_end(&mut buf_sync).unwrap(); + } + + let mut buf_async = vec![]; + { + let input = ArmoredReader::from_async_reader(&encoded_async[..]); + pin_mut!(input); + + let mut cx = noop_context(); + + let mut tmp = [0; 4096]; + loop { + match input.as_mut().poll_read(&mut cx, &mut tmp) { + Poll::Ready(Ok(0)) => break, + Poll::Ready(Ok(read)) => buf_async.extend_from_slice(&tmp[..read]), + Poll::Ready(Err(e)) => panic!("Unexpected error: {}", e), + Poll::Pending => panic!("Unexpected Pending"), + } + } + } + + assert_eq!(buf_async, data); + assert_eq!(buf_sync, data); + } } From f6e7d9ab29accc260c1b709fe36279040c1aaa3c Mon Sep 17 00:00:00 2001 From: Dario Date: Thu, 20 Feb 2025 13:15:19 +0400 Subject: [PATCH 02/20] age: armor: Fix AsyncWrite chucked encoding --- age/src/primitives/armor.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/age/src/primitives/armor.rs b/age/src/primitives/armor.rs index 7dac376a..841acd56 100644 --- a/age/src/primitives/armor.rs +++ b/age/src/primitives/armor.rs @@ -517,11 +517,11 @@ impl AsyncWrite for ArmoredWriter { BASE64_STANDARD .encode_slice(&byte_buf, &mut encoded_buf[..],) .expect("byte_buf.len() <= BASE64_CHUNK_SIZE_BYTES"), - ARMORED_COLUMNS_PER_LINE + BASE64_CHUNK_SIZE_COLUMNS ); *encoded_line = Some(EncodedBytes { offset: 0, - end: ARMORED_COLUMNS_PER_LINE, + end: BASE64_CHUNK_SIZE_COLUMNS, }); byte_buf.clear(); } From 8cb833809bb0e4c3cbdcc1b8095e973cbff25ed5 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Sun, 7 Dec 2025 14:07:37 +0000 Subject: [PATCH 03/20] age: Test that encrypted identity decryption fails if we can't get a passphrase --- age/src/encrypted.rs | 42 ++++++++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/age/src/encrypted.rs b/age/src/encrypted.rs index 4570a2bd..561cfdea 100644 --- a/age/src/encrypted.rs +++ b/age/src/encrypted.rs @@ -216,10 +216,10 @@ fOrxrKTj7xCdNS3+OrCdnBC8Z9cKDxjCGWW3fkjLsYha0Jo= const TEST_RECIPIENT: &str = "age1ysxuaeqlk7xd8uqsh8lsnfwt9jzzjlqf49ruhpjrrj5yatlcuf7qke4pqe"; #[derive(Clone)] - struct MockCallbacks(Arc>>); + struct MockCallbacks(Arc>>>); impl MockCallbacks { - fn new(passphrase: &'static str) -> Self { + fn new(passphrase: Option<&'static str>) -> Self { MockCallbacks(Arc::new(Mutex::new(Some(passphrase)))) } } @@ -239,9 +239,13 @@ fOrxrKTj7xCdNS3+OrCdnBC8Z9cKDxjCGWW3fkjLsYha0Jo= /// This intentionally panics if called twice. fn request_passphrase(&self, _: &str) -> Option { - Some(SecretString::from( - self.0.lock().unwrap().take().unwrap().to_owned(), - )) + self.0 + .lock() + .unwrap() + .take() + .expect("passphrase is only input once") + .to_owned() + .map(SecretString::from) } } @@ -258,10 +262,28 @@ fOrxrKTj7xCdNS3+OrCdnBC8Z9cKDxjCGWW3fkjLsYha0Jo= // Unwrapping with the wrong passphrase fails. { let buf = ArmoredReader::new(TEST_ENCRYPTED_IDENTITY.as_bytes()); - let identity = - Identity::from_buffer(buf, None, MockCallbacks::new("wrong passphrase"), None) - .unwrap() - .unwrap(); + let identity = Identity::from_buffer( + buf, + None, + MockCallbacks::new(Some("wrong passphrase")), + None, + ) + .unwrap() + .unwrap(); + + if let Err(e) = identity.unwrap_stanzas(&wrapped).unwrap() { + assert!(matches!(e, DecryptError::KeyDecryptionFailed)); + } else { + panic!("Should have failed"); + } + } + + // Unwrapping fails if we cannot obtain a passphrase. + { + let buf = ArmoredReader::new(TEST_ENCRYPTED_IDENTITY.as_bytes()); + let identity = Identity::from_buffer(buf, None, MockCallbacks::new(None), None) + .unwrap() + .unwrap(); if let Err(e) = identity.unwrap_stanzas(&wrapped).unwrap() { assert!(matches!(e, DecryptError::KeyDecryptionFailed)); @@ -274,7 +296,7 @@ fOrxrKTj7xCdNS3+OrCdnBC8Z9cKDxjCGWW3fkjLsYha0Jo= let identity = Identity::from_buffer( buf, None, - MockCallbacks::new(TEST_ENCRYPTED_IDENTITY_PASSPHRASE), + MockCallbacks::new(Some(TEST_ENCRYPTED_IDENTITY_PASSPHRASE)), None, ) .unwrap() From e6bdbe6536039976ea5eb8797196277b9716dc4b Mon Sep 17 00:00:00 2001 From: Lucas Desgouilles Date: Sun, 26 Jan 2025 15:39:19 +0100 Subject: [PATCH 04/20] Return a key decryption failure error when the user provides no password Currently it just panics with no good message --- age/src/encrypted.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/age/src/encrypted.rs b/age/src/encrypted.rs index 561cfdea..aecae172 100644 --- a/age/src/encrypted.rs +++ b/age/src/encrypted.rs @@ -43,7 +43,7 @@ impl IdentityState { filename = filename.unwrap_or_default() )) { Some(passphrase) => passphrase, - None => todo!(), + None => Err(DecryptError::KeyDecryptionFailed)?, }; let mut identity = scrypt::Identity::new(passphrase); From dfa5dcaa9fdb62933c8f246d8c6d685efa507c59 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Sun, 7 Dec 2025 18:04:26 +0000 Subject: [PATCH 05/20] age: Test that the empty plugin name is rejected --- age/src/plugin.rs | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/age/src/plugin.rs b/age/src/plugin.rs index f38ec0cf..8bf1bac9 100644 --- a/age/src/plugin.rs +++ b/age/src/plugin.rs @@ -753,6 +753,13 @@ mod tests { ); } + #[test] + fn recipient_rejects_empty_name() { + let invalid_recipient = + bech32::encode(PLUGIN_RECIPIENT_PREFIX, [], bech32::Variant::Bech32).unwrap(); + assert!(invalid_recipient.parse::().is_err()); + } + #[test] fn recipient_rejects_invalid_chars() { let invalid_recipient = bech32::encode( @@ -764,6 +771,18 @@ mod tests { assert!(invalid_recipient.parse::().is_err()); } + #[test] + fn identity_rejects_empty_name() { + let invalid_identity = bech32::encode( + &format!("{}-", PLUGIN_IDENTITY_PREFIX), + [], + bech32::Variant::Bech32, + ) + .expect("HRP is valid") + .to_uppercase(); + assert!(invalid_identity.parse::().is_err()); + } + #[test] fn identity_rejects_invalid_chars() { let invalid_identity = bech32::encode( @@ -776,12 +795,26 @@ mod tests { assert!(invalid_identity.parse::().is_err()); } + #[test] + #[should_panic] + fn identity_default_for_plugin_rejects_empty_name() { + Identity::default_for_plugin(""); + } + #[test] #[should_panic] fn identity_default_for_plugin_rejects_invalid_chars() { Identity::default_for_plugin(INVALID_PLUGIN_NAME); } + #[test] + fn recipient_plugin_v1_rejects_empty_name() { + assert!(matches!( + RecipientPluginV1::new("", &[], &[], NoCallbacks), + Err(EncryptError::MissingPlugin { binary_name }) if binary_name.is_empty(), + )); + } + #[test] fn recipient_plugin_v1_rejects_invalid_chars() { assert!(matches!( @@ -790,6 +823,14 @@ mod tests { )); } + #[test] + fn identity_plugin_v1_rejects_empty_name() { + assert!(matches!( + IdentityPluginV1::new("", &[], NoCallbacks), + Err(DecryptError::MissingPlugin { binary_name }) if binary_name.is_empty(), + )); + } + #[test] fn identity_plugin_v1_rejects_invalid_chars() { assert!(matches!( From ceb6faa2b1c5788fed4a26a291c631bf179db9a2 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Sun, 7 Dec 2025 18:09:59 +0000 Subject: [PATCH 06/20] age: Reject empty plugin name `age::plugin::Recipient` was already incidentally enforcing this, but the other types within `age::plugin` were not. Closes str4d/rage#564. --- age/CHANGELOG.md | 4 ++++ age/src/plugin.rs | 1 + 2 files changed, 5 insertions(+) diff --git a/age/CHANGELOG.md b/age/CHANGELOG.md index eb79b409..759e556f 100644 --- a/age/CHANGELOG.md +++ b/age/CHANGELOG.md @@ -10,6 +10,10 @@ to 1.0.0 are beta releases. ## [Unreleased] +### Fixed +- `age::plugin::{Identity, RecipientPluginV1, IdentityPluginV1}` now correctly + reject the empty plugin name (like `age::plugin::Recipient` already was). + ## [0.6.1, 0.7.2, 0.8.2, 0.9.3, 0.10.1, 0.11.1] - 2024-11-18 ### Security - Fixed a security vulnerability that could allow an attacker to execute an diff --git a/age/src/plugin.rs b/age/src/plugin.rs index 8bf1bac9..c74d83c7 100644 --- a/age/src/plugin.rs +++ b/age/src/plugin.rs @@ -48,6 +48,7 @@ fn valid_plugin_name(plugin_name: &str) -> bool { plugin_name .bytes() .all(|b| b.is_ascii_alphanumeric() | matches!(b, b'+' | b'-' | b'.' | b'_')) + && !plugin_name.is_empty() } fn binary_name(plugin_name: &str) -> String { From 1465e602c83c5e4d3db8f58d9bdfc2a10063f90d Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Sun, 7 Dec 2025 20:25:14 +0000 Subject: [PATCH 07/20] age 0.11.2 --- Cargo.lock | 2 +- age/CHANGELOG.md | 5 +++++ age/Cargo.toml | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 72e96679..7faabc9a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -60,7 +60,7 @@ dependencies = [ [[package]] name = "age" -version = "0.11.1" +version = "0.11.2" dependencies = [ "aes", "aes-gcm", diff --git a/age/CHANGELOG.md b/age/CHANGELOG.md index 759e556f..5cfe5988 100644 --- a/age/CHANGELOG.md +++ b/age/CHANGELOG.md @@ -10,7 +10,12 @@ to 1.0.0 are beta releases. ## [Unreleased] +## [0.11.2] - 2025-12-07 ### Fixed +- `age::armor::ArmoredWriter::poll_write` no longer panics when writing more + than 6144 bytes. +- `age::encrypted::Identity` no longer causes a panic when being decrypted if + the `age::Callbacks::request_passphrase` impl returns `None`. - `age::plugin::{Identity, RecipientPluginV1, IdentityPluginV1}` now correctly reject the empty plugin name (like `age::plugin::Recipient` already was). diff --git a/age/Cargo.toml b/age/Cargo.toml index 11510461..994ca3f5 100644 --- a/age/Cargo.toml +++ b/age/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "age" description = "[BETA] A simple, secure, and modern encryption library." -version = "0.11.1" +version = "0.11.2" authors.workspace = true repository.workspace = true readme = "README.md" From 1ff02de747097f31f47c3968d586118c7b57da08 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Tue, 21 Apr 2026 20:49:40 +0000 Subject: [PATCH 08/20] age: Return error instead of panicking on empty passphrase --- age/CHANGELOG.md | 3 +++ age/src/ssh.rs | 45 +++++++++++++++++++++++++++------------------ 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/age/CHANGELOG.md b/age/CHANGELOG.md index 5cfe5988..58219ec7 100644 --- a/age/CHANGELOG.md +++ b/age/CHANGELOG.md @@ -9,6 +9,9 @@ and this project adheres to Rust's notion of to 1.0.0 are beta releases. ## [Unreleased] +### Fixed +- `age::ssh::EncryptedKey::decrypt` now returns an error instead of panicking + when given an empty passphrase. ## [0.11.2] - 2025-12-07 ### Fixed diff --git a/age/src/ssh.rs b/age/src/ssh.rs index fadab56e..be9bf58d 100644 --- a/age/src/ssh.rs +++ b/age/src/ssh.rs @@ -76,9 +76,9 @@ impl OpenSshCipher { ) -> Result, DecryptError> { match self { OpenSshCipher::Aes256Cbc => decrypt::aes_cbc::(kdf, p, ct), - OpenSshCipher::Aes128Ctr => Ok(decrypt::aes_ctr::(kdf, p, ct)), - OpenSshCipher::Aes192Ctr => Ok(decrypt::aes_ctr::(kdf, p, ct)), - OpenSshCipher::Aes256Ctr => Ok(decrypt::aes_ctr::(kdf, p, ct)), + OpenSshCipher::Aes128Ctr => decrypt::aes_ctr::(kdf, p, ct), + OpenSshCipher::Aes192Ctr => decrypt::aes_ctr::(kdf, p, ct), + OpenSshCipher::Aes256Ctr => decrypt::aes_ctr::(kdf, p, ct), OpenSshCipher::Aes256Gcm => decrypt::aes_gcm::(kdf, p, ct), } } @@ -91,13 +91,15 @@ enum OpenSshKdf { } impl OpenSshKdf { - fn derive(&self, passphrase: SecretString, out_len: usize) -> Vec { + fn derive(&self, passphrase: SecretString, out_len: usize) -> Option> { match self { OpenSshKdf::Bcrypt { salt, rounds } => { let mut output = vec![0; out_len]; bcrypt_pbkdf(passphrase.expose_secret(), salt, *rounds, &mut output) - .expect("parameters are valid"); - output + // The only error that can occur is if `passphrase` is empty. All + // other errors are prevented by construction. + .ok() + .map(|()| output) } } } @@ -147,13 +149,17 @@ mod decrypt { fn derive_key_material, IvSize: ArrayLength>( kdf: &OpenSshKdf, passphrase: SecretString, - ) -> (GenericArray, GenericArray) { - let kdf_output = kdf.derive(passphrase, KeySize::USIZE + IvSize::USIZE); - let (key, iv) = kdf_output.split_at(KeySize::USIZE); - ( - GenericArray::from_exact_iter(key.iter().copied()).expect("key is correct length"), - GenericArray::from_exact_iter(iv.iter().copied()).expect("iv is correct length"), - ) + ) -> Option<(GenericArray, GenericArray)> { + kdf.derive(passphrase, KeySize::USIZE + IvSize::USIZE) + .map(|kdf_output| { + let (key, iv) = kdf_output.split_at(KeySize::USIZE); + ( + GenericArray::from_exact_iter(key.iter().copied()) + .expect("key is correct length"), + GenericArray::from_exact_iter(iv.iter().copied()) + .expect("iv is correct length"), + ) + }) } pub(super) fn aes_cbc( @@ -161,7 +167,8 @@ mod decrypt { passphrase: SecretString, ciphertext: &[u8], ) -> Result, DecryptError> { - let (key, iv) = derive_key_material::(kdf, passphrase); + let (key, iv) = derive_key_material::(kdf, passphrase) + .ok_or(DecryptError::KeyDecryptionFailed)?; let cipher = C::new(&key, &iv); cipher .decrypt_padded_vec_mut::(ciphertext) @@ -172,12 +179,13 @@ mod decrypt { kdf: &OpenSshKdf, passphrase: SecretString, ciphertext: &[u8], - ) -> Vec { - let (key, iv) = derive_key_material::(kdf, passphrase); + ) -> Result, DecryptError> { + let (key, iv) = derive_key_material::(kdf, passphrase) + .ok_or(DecryptError::KeyDecryptionFailed)?; let mut cipher = C::new(&key, &iv); let mut plaintext = ciphertext.to_vec(); cipher.apply_keystream(&mut plaintext); - plaintext + Ok(plaintext) } pub(super) fn aes_gcm( @@ -185,7 +193,8 @@ mod decrypt { passphrase: SecretString, ciphertext: &[u8], ) -> Result, DecryptError> { - let (key, nonce) = derive_key_material::(kdf, passphrase); + let (key, nonce) = derive_key_material::(kdf, passphrase) + .ok_or(DecryptError::KeyDecryptionFailed)?; let mut cipher = C::new(&key); cipher .decrypt(&nonce, ciphertext) From 582247dbc84424091e1901b1c6294fc44daf8607 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Tue, 21 Apr 2026 22:38:31 +0000 Subject: [PATCH 09/20] age: Fix panic in debug mode on truncated ciphertext In release mode the underflow resulted in a seek beyond the end of the truncated ciphertext, which is valid and results in a zero-length read during last-chunk validation (resulting in the intended error). --- age/CHANGELOG.md | 2 ++ age/src/primitives/stream.rs | 53 ++++++++++++++++++++++++++++++++++-- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/age/CHANGELOG.md b/age/CHANGELOG.md index 58219ec7..8361ff75 100644 --- a/age/CHANGELOG.md +++ b/age/CHANGELOG.md @@ -12,6 +12,8 @@ to 1.0.0 are beta releases. ### Fixed - `age::ssh::EncryptedKey::decrypt` now returns an error instead of panicking when given an empty passphrase. +- `age::stream::StreamReader` no longer panics in debug mode when seeking on a + ciphertext truncated to just after the nonce (i.e. with zero chunk data). ## [0.11.2] - 2025-12-07 ### Fixed diff --git a/age/src/primitives/stream.rs b/age/src/primitives/stream.rs index 880084d0..4453abd2 100644 --- a/age/src/primitives/stream.rs +++ b/age/src/primitives/stream.rs @@ -565,13 +565,22 @@ impl StreamReader { let num_chunks = (ct_len + (ENCRYPTED_CHUNK_SIZE as u64 - 1)) / ENCRYPTED_CHUNK_SIZE as u64; + // If we have no ciphertext data then there is no last chunk, which is + // invalid. + let non_last_chunks = num_chunks.checked_sub(1).ok_or_else(|| { + io::Error::new( + io::ErrorKind::InvalidData, + "Last chunk is invalid, stream might be truncated", + ) + })?; + // Authenticate the ciphertext length by checking that we can successfully // decrypt the last chunk _as_ a last chunk. - let last_chunk_start = ct_start + ((num_chunks - 1) * ENCRYPTED_CHUNK_SIZE as u64); + let last_chunk_start = ct_start + (non_last_chunks * ENCRYPTED_CHUNK_SIZE as u64); let mut last_chunk = Vec::with_capacity((ct_end - last_chunk_start) as usize); self.inner.seek(SeekFrom::Start(last_chunk_start))?; self.inner.read_to_end(&mut last_chunk)?; - self.stream.nonce.set_counter(num_chunks - 1); + self.stream.nonce.set_counter(non_last_chunks); self.stream.decrypt_chunk(&last_chunk, true).map_err(|_| { io::Error::new( io::ErrorKind::InvalidData, @@ -679,7 +688,7 @@ mod tests { use age_core::secrecy::ExposeSecret; use std::io::{self, Cursor, Read, Seek, SeekFrom, Write}; - use super::{PayloadKey, Stream, CHUNK_SIZE}; + use super::{PayloadKey, Stream, CHUNK_SIZE, TAG_SIZE}; #[cfg(feature = "async")] use futures::{ @@ -1019,6 +1028,44 @@ mod tests { } } + #[test] + fn seek_from_end_with_empty_fails_on_truncation() { + // The empty plaintext means we should encrypt to a single chunk. + let plaintext: Vec = b"".to_vec(); + + // Encrypt the plaintext just like the example code in the docs. + let mut encrypted = vec![]; + { + let mut w = Stream::encrypt(PayloadKey([7; 32].into()), &mut encrypted); + w.write_all(&plaintext).unwrap(); + w.finish().unwrap(); + }; + assert_eq!(encrypted.len(), TAG_SIZE); + + // Every truncated length should result in an error. + for i in 0..TAG_SIZE { + let truncated_ciphertext = &encrypted[..i]; + let mut truncated_reader = Stream::decrypt( + PayloadKey([7; 32].into()), + Cursor::new(truncated_ciphertext), + ); + match truncated_reader.seek(SeekFrom::End(0)) { + Err(e) => { + assert_eq!(e.kind(), io::ErrorKind::InvalidData); + assert_eq!( + &e.to_string(), + "Last chunk is invalid, stream might be truncated", + ); + } + Ok(_) => panic!("This is a security issue."), + } + } + + // Decrypting without truncation should show an empty file. + let mut reader = Stream::decrypt(PayloadKey([7; 32].into()), Cursor::new(encrypted)); + assert_eq!(reader.len().unwrap(), 0); + } + #[test] fn seek_from_end_with_exact_chunk() { let plaintext: Vec = vec![42; 65536]; From 4e6ce24e9b2b84133f5b592da1e4191df67ba514 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Tue, 21 Apr 2026 22:59:42 +0000 Subject: [PATCH 10/20] age: Document security implication of `scrypt::Identity::set_max_work_factor` --- age/src/scrypt.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/age/src/scrypt.rs b/age/src/scrypt.rs index 60938e86..fb62dc43 100644 --- a/age/src/scrypt.rs +++ b/age/src/scrypt.rs @@ -200,6 +200,13 @@ impl Identity { /// /// This method must be called before [`Self::unwrap_stanza`] to have an effect. /// + /// # Security + /// + /// This sets the bounds on CPU/memory cost. Large values (e.g. > 22) can allow + /// attempted decryption of a malicious file that takes hours and tens of GiB of RAM. + /// When setting this value in your application, take care to limit it appropriately + /// and avoid Denial-of-Service issues. + /// /// [`Self::unwrap_stanza`]: crate::Identity::unwrap_stanza pub fn set_max_work_factor(&mut self, max_log_n: u8) { self.max_work_factor = max_log_n; From fc4164ff5052db9484b99b63655d3560129c5be8 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Tue, 21 Apr 2026 23:18:39 +0000 Subject: [PATCH 11/20] age: Fix panics on weird error formats in plugin responses --- age/CHANGELOG.md | 2 ++ age/src/error.rs | 11 ++++++++-- age/src/plugin.rs | 55 ++++++++++++++++++++++++++++++++--------------- 3 files changed, 49 insertions(+), 19 deletions(-) diff --git a/age/CHANGELOG.md b/age/CHANGELOG.md index 8361ff75..620f502f 100644 --- a/age/CHANGELOG.md +++ b/age/CHANGELOG.md @@ -10,6 +10,8 @@ to 1.0.0 are beta releases. ## [Unreleased] ### Fixed +- `age::plugin::{RecipientPluginV1, IdentityPluginV1}` no longer panic when a + plugin sends an unusually-formatted error in phase 2. - `age::ssh::EncryptedKey::decrypt` now returns an error instead of panicking when given an empty passphrase. - `age::stream::StreamReader` no longer panics in debug mode when seeking on a diff --git a/age/src/error.rs b/age/src/error.rs index 5505d4dd..939bae22 100644 --- a/age/src/error.rs +++ b/age/src/error.rs @@ -9,6 +9,9 @@ use crate::{wfl, wlnfl}; #[cfg(feature = "plugin")] use age_core::format::Stanza; +#[cfg(feature = "plugin")] +use crate::plugin::CMD_ERROR; + /// Errors returned when converting an identity file to a recipients file. #[derive(Debug)] pub enum IdentityFileConvertError { @@ -110,8 +113,12 @@ pub enum PluginError { #[cfg(feature = "plugin")] impl From for PluginError { fn from(mut s: Stanza) -> Self { - assert!(s.tag == "error"); - let kind = s.args.remove(0); + assert_eq!(s.tag, CMD_ERROR); + let kind = if s.args.is_empty() { + "unknown".into() + } else { + s.args.remove(0) + }; PluginError::Other { kind, metadata: s.args, diff --git a/age/src/plugin.rs b/age/src/plugin.rs index c74d83c7..4b7a13cc 100644 --- a/age/src/plugin.rs +++ b/age/src/plugin.rs @@ -31,7 +31,7 @@ use crate::{ const PLUGIN_RECIPIENT_PREFIX: &str = "age1"; const PLUGIN_IDENTITY_PREFIX: &str = "age-plugin-"; -const CMD_ERROR: &str = "error"; +pub(crate) const CMD_ERROR: &str = "error"; const CMD_RECIPIENT_STANZA: &str = "recipient-stanza"; const CMD_LABELS: &str = "labels"; const CMD_MSG: &str = "msg"; @@ -531,18 +531,32 @@ impl crate::Recipient for RecipientPluginV1 { } CMD_ERROR => { if command.args.len() == 2 && command.args[0] == "recipient" { - let index: usize = command.args[1].parse().unwrap(); - errors.push(PluginError::Recipient { - binary_name: binary_name(&self.recipients[index].name), - recipient: self.recipients[index].recipient.clone(), - message: String::from_utf8_lossy(&command.body).to_string(), - }); + if let Some(r) = command.args[1] + .parse() + .ok() + .and_then(|index: usize| self.recipients.get(index)) + { + errors.push(PluginError::Recipient { + binary_name: binary_name(&r.name), + recipient: r.recipient.clone(), + message: String::from_utf8_lossy(&command.body).to_string(), + }); + } else { + errors.push(PluginError::from(command)); + } } else if command.args.len() == 2 && command.args[0] == "identity" { - let index: usize = command.args[1].parse().unwrap(); - errors.push(PluginError::Identity { - binary_name: binary_name(&self.identities[index].name), - message: String::from_utf8_lossy(&command.body).to_string(), - }); + if let Some(identity) = command.args[1] + .parse() + .ok() + .and_then(|index: usize| self.identities.get(index)) + { + errors.push(PluginError::Identity { + binary_name: binary_name(&identity.name), + message: String::from_utf8_lossy(&command.body).to_string(), + }); + } else { + errors.push(PluginError::from(command)); + } } else { errors.push(PluginError::from(command)); } @@ -701,11 +715,18 @@ impl IdentityPluginV1 { } CMD_ERROR => { if command.args.len() == 2 && command.args[0] == "identity" { - let index: usize = command.args[1].parse().unwrap(); - errors.push(PluginError::Identity { - binary_name: binary_name(&self.identities[index].name), - message: String::from_utf8_lossy(&command.body).to_string(), - }); + if let Some(identity) = command.args[1] + .parse() + .ok() + .and_then(|index: usize| self.identities.get(index)) + { + errors.push(PluginError::Identity { + binary_name: binary_name(&identity.name), + message: String::from_utf8_lossy(&command.body).to_string(), + }); + } else { + errors.push(PluginError::from(command)); + } } else { errors.push(PluginError::from(command)); } From cd2e00d4625263e12f66ede6831fa4f9a13eb4fc Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Tue, 21 Apr 2026 23:31:32 +0000 Subject: [PATCH 12/20] age: Replace file-key panics with errors in `plugin::IdentityPluginV1` --- age/CHANGELOG.md | 8 ++++++-- age/src/plugin.rs | 26 ++++++++++++++------------ 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/age/CHANGELOG.md b/age/CHANGELOG.md index 620f502f..1ee009a9 100644 --- a/age/CHANGELOG.md +++ b/age/CHANGELOG.md @@ -10,8 +10,12 @@ to 1.0.0 are beta releases. ## [Unreleased] ### Fixed -- `age::plugin::{RecipientPluginV1, IdentityPluginV1}` no longer panic when a - plugin sends an unusually-formatted error in phase 2. +- `age::plugin`: + - `{RecipientPluginV1, IdentityPluginV1}` no longer panic when a plugin sends + an unusually-formatted error in phase 2. + - `IdentityPluginV1` no longer panics when a plugin violates the specification + and returns a file key for a file index that was not provided, or sends more + than one file key per file index. - `age::ssh::EncryptedKey::decrypt` now returns an error instead of panicking when given an empty passphrase. - `age::stream::StreamReader` no longer panics in debug mode when seeking on a diff --git a/age/src/plugin.rs b/age/src/plugin.rs index 4b7a13cc..0fed6753 100644 --- a/age/src/plugin.rs +++ b/age/src/plugin.rs @@ -700,18 +700,20 @@ impl IdentityPluginV1 { } } CMD_FILE_KEY => { - // We only support a single file. - assert!(command.args[0] == "0"); - assert!(file_key.is_none()); - file_key = Some(FileKey::try_init_with_mut(|file_key| { - if command.body.len() == file_key.len() { - file_key.copy_from_slice(&command.body); - Ok(()) - } else { - Err(DecryptError::DecryptionFailed) - } - })); - reply.ok(None) + // We only requested one file key be unwrapped. + if command.args.len() == 1 && command.args[0] == "0" && file_key.is_none() { + file_key = Some(FileKey::try_init_with_mut(|file_key| { + if command.body.len() == file_key.len() { + file_key.copy_from_slice(&command.body); + Ok(()) + } else { + Err(DecryptError::DecryptionFailed) + } + })); + reply.ok(None) + } else { + reply.fail() + } } CMD_ERROR => { if command.args.len() == 2 && command.args[0] == "identity" { From 85c747cbab1f0b8a0cdcfba80cb4da581f0fc3cf Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Tue, 21 Apr 2026 23:44:07 +0000 Subject: [PATCH 13/20] age: Zeroize intermediate buffer when parsing `x25519::Identity` --- age/src/x25519.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/age/src/x25519.rs b/age/src/x25519.rs index 98edb15b..86cc678a 100644 --- a/age/src/x25519.rs +++ b/age/src/x25519.rs @@ -42,12 +42,17 @@ impl std::str::FromStr for Identity { fn from_str(s: &str) -> Result { parse_bech32(s) .ok_or("invalid Bech32 encoding") - .and_then(|(hrp, bytes)| { + .and_then(|(hrp, mut bytes)| { if hrp == SECRET_KEY_PREFIX { - TryInto::<[u8; 32]>::try_into(&bytes[..]) + let identity = TryInto::<[u8; 32]>::try_into(&bytes[..]) .map_err(|_| "incorrect identity length") .map(StaticSecret::from) - .map(Identity) + .map(Identity); + + // Clear intermediates + bytes.zeroize(); + + identity } else { Err("incorrect HRP") } From 6fe47a2aa1306377352da242d245dfe88e021450 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Wed, 22 Apr 2026 00:06:22 +0000 Subject: [PATCH 14/20] age: Add some zeroization to `IdentityFile` We can't zeroize inside `std::io::BufReader`, but we can zeroize each line it gives us. --- age/src/identity.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/age/src/identity.rs b/age/src/identity.rs index 6bd19d65..d4269496 100644 --- a/age/src/identity.rs +++ b/age/src/identity.rs @@ -1,6 +1,8 @@ use std::fs::File; use std::io; +use zeroize::Zeroize; + use crate::{x25519, Callbacks, DecryptError, EncryptError, IdentityFileConvertError, NoCallbacks}; #[cfg(feature = "cli-common")] @@ -71,7 +73,7 @@ impl IdentityFile { let mut identities = vec![]; for (line_number, line) in data.lines().enumerate() { - let line = line?; + let mut line = line?; if line.is_empty() || line.starts_with('#') { continue; } @@ -96,6 +98,8 @@ impl IdentityFile { #[cfg(not(feature = "plugin"))] let _: () = identity; } else { + line.zeroize(); + // Return a line number in place of the line, so we don't leak the file // contents in error messages. return Err(io::Error::new( @@ -114,6 +118,8 @@ impl IdentityFile { }, )); } + + line.zeroize(); } Ok(IdentityFile { From 8e5145f01916d11519f3b19b37aa70f0a95e8cf2 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Wed, 22 Apr 2026 00:25:22 +0000 Subject: [PATCH 15/20] age: Limit recipient and identity files to 16 MiB --- age/CHANGELOG.md | 5 ++++ age/src/cli_common/identities.rs | 2 ++ age/src/cli_common/recipients.rs | 7 +++++- age/src/identity.rs | 17 ++++++++++--- age/src/util.rs | 43 ++++++++++++++++++++++++++++++++ 5 files changed, 70 insertions(+), 4 deletions(-) diff --git a/age/CHANGELOG.md b/age/CHANGELOG.md index 1ee009a9..094bbc1c 100644 --- a/age/CHANGELOG.md +++ b/age/CHANGELOG.md @@ -9,6 +9,11 @@ and this project adheres to Rust's notion of to 1.0.0 are beta releases. ## [Unreleased] +### Changed +- Recipient and identity files (parsed via `age::IdentityFile` or + `age::cli_common::{read_recipients, read_identities}`) is now limited to at + most 16 MiB, matching the Go implementation. + ### Fixed - `age::plugin`: - `{RecipientPluginV1, IdentityPluginV1}` no longer panic when a plugin sends diff --git a/age/src/cli_common/identities.rs b/age/src/cli_common/identities.rs index e9625a37..b6020124 100644 --- a/age/src/cli_common/identities.rs +++ b/age/src/cli_common/identities.rs @@ -11,6 +11,8 @@ use crate::{armor::ArmoredReader, cli_common::file_io::InputReader}; /// `filenames` may contain at most one entry of `"-"`, which will be interpreted as /// reading from standard input. An error will be returned if `stdin_guard` is guarding an /// existing usage of standard input. +/// +/// Each file in `filenames` may be at most 16 MiB. pub fn read_identities( filenames: Vec, max_work_factor: Option, diff --git a/age/src/cli_common/recipients.rs b/age/src/cli_common/recipients.rs index 913a8326..79c79c57 100644 --- a/age/src/cli_common/recipients.rs +++ b/age/src/cli_common/recipients.rs @@ -3,6 +3,7 @@ use std::io::{self, BufReader}; use super::StdinGuard; use super::{identities::parse_identity_files, ReadError}; use crate::identity::RecipientsAccumulator; +use crate::util::LimitedReader; use crate::{x25519, Recipient}; #[cfg(feature = "plugin")] @@ -17,6 +18,8 @@ use crate::ssh; #[cfg(any(feature = "armor", feature = "plugin"))] use crate::EncryptError; +const RECIPIENT_FILE_SIZE_LIMIT: usize = 1 << 24; // 16 MiB + /// Handles error mapping for the given SSH recipient parser. /// /// Returns `Ok(None)` if the parser finds a parseable value that should be ignored. This @@ -126,6 +129,8 @@ fn read_recipients_list( /// `recipients_file_strings` and `identity_strings` may collectively contain at most one /// entry of `"-"`, which will be interpreted as reading from standard input. An error /// will be returned if `stdin_guard` is guarding an existing usage of standard input. +/// +/// Each file in `recipients_file_strings` and `identity_strings` may be at most 16 MiB. pub fn read_recipients( recipient_strings: Vec, recipients_file_strings: Vec, @@ -146,7 +151,7 @@ pub fn read_recipients( } _ => e, })?; - let buf = BufReader::new(f); + let buf = LimitedReader::new(BufReader::new(f), RECIPIENT_FILE_SIZE_LIMIT); read_recipients_list(&arg, buf, &mut recipients)?; } diff --git a/age/src/identity.rs b/age/src/identity.rs index d4269496..8c162b61 100644 --- a/age/src/identity.rs +++ b/age/src/identity.rs @@ -1,9 +1,14 @@ -use std::fs::File; -use std::io; +use std::{ + fs::File, + io::{self, BufRead}, +}; use zeroize::Zeroize; -use crate::{x25519, Callbacks, DecryptError, EncryptError, IdentityFileConvertError, NoCallbacks}; +use crate::{ + util::LimitedReader, x25519, Callbacks, DecryptError, EncryptError, IdentityFileConvertError, + NoCallbacks, +}; #[cfg(feature = "cli-common")] use crate::cli_common::file_io::InputReader; @@ -11,6 +16,8 @@ use crate::cli_common::file_io::InputReader; #[cfg(feature = "plugin")] use crate::plugin; +const IDENTITY_SIZE_LIMIT: usize = 1 << 24; // 16 MiB + /// The supported kinds of identities within an [`IdentityFile`]. #[derive(Clone)] enum IdentityFileEntry { @@ -43,6 +50,8 @@ impl IdentityFileEntry { } /// A list of identities that has been parsed from some input file. +/// +/// The maximum supported file size is 16 MiB. pub struct IdentityFile { filename: Option, identities: Vec, @@ -72,6 +81,8 @@ impl IdentityFile { fn parse_identities(filename: Option, data: R) -> io::Result { let mut identities = vec![]; + let data = LimitedReader::new(data, IDENTITY_SIZE_LIMIT); + for (line_number, line) in data.lines().enumerate() { let mut line = line?; if line.is_empty() || line.starts_with('#') { diff --git a/age/src/util.rs b/age/src/util.rs index ed8ec28d..c4d70b47 100644 --- a/age/src/util.rs +++ b/age/src/util.rs @@ -1,3 +1,5 @@ +use std::io; + use bech32::{FromBase32, Variant}; #[cfg(all(any(feature = "armor", feature = "cli-common"), windows))] @@ -15,6 +17,47 @@ pub(crate) fn parse_bech32(s: &str) -> Option<(String, Vec)> { }) } +pub(crate) struct LimitedReader { + inner: R, + n: usize, +} +impl LimitedReader { + pub(crate) fn new(reader: R, n: usize) -> Self { + Self { inner: reader, n } + } +} + +impl io::Read for LimitedReader { + fn read(&mut self, mut buf: &mut [u8]) -> io::Result { + if self.n == 0 { + Ok(0) + } else { + if buf.len() > self.n { + buf = &mut buf[..self.n]; + } + let read = self.inner.read(buf)?; + self.n -= read; + Ok(read) + } + } +} + +impl io::BufRead for LimitedReader { + fn fill_buf(&mut self) -> io::Result<&[u8]> { + if self.n == 0 { + Ok(&[]) + } else { + let buf = self.inner.fill_buf()?; + Ok(&buf[..buf.len().min(self.n)]) + } + } + + fn consume(&mut self, amount: usize) { + self.n -= amount; + self.inner.consume(amount); + } +} + pub(crate) mod read { use std::str::FromStr; From e3cf83d83788ff5f5a9ba8473e4bc64d674e2b23 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Wed, 22 Apr 2026 00:45:17 +0000 Subject: [PATCH 16/20] age: Limit SSH keys to 16 kiB --- age/CHANGELOG.md | 2 ++ age/src/cli_common/identities.rs | 13 +++++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/age/CHANGELOG.md b/age/CHANGELOG.md index 094bbc1c..06715f88 100644 --- a/age/CHANGELOG.md +++ b/age/CHANGELOG.md @@ -13,6 +13,8 @@ to 1.0.0 are beta releases. - Recipient and identity files (parsed via `age::IdentityFile` or `age::cli_common::{read_recipients, read_identities}`) is now limited to at most 16 MiB, matching the Go implementation. +- `age::cli_common::read_identities` now limits SSH keys to at most 16 kiB, + matching the Go implementation. ### Fixed - `age::plugin`: diff --git a/age/src/cli_common/identities.rs b/age/src/cli_common/identities.rs index b6020124..d0aff0ec 100644 --- a/age/src/cli_common/identities.rs +++ b/age/src/cli_common/identities.rs @@ -6,13 +6,19 @@ use crate::{identity::IdentityFile, Identity}; #[cfg(feature = "armor")] use crate::{armor::ArmoredReader, cli_common::file_io::InputReader}; +#[cfg(feature = "ssh")] +use crate::util::LimitedReader; + +#[cfg(feature = "ssh")] +const SSH_IDENTITY_SIZE_LIMIT: usize = 1 << 14; // 16 KiB + /// Reads identities from the provided files. /// /// `filenames` may contain at most one entry of `"-"`, which will be interpreted as /// reading from standard input. An error will be returned if `stdin_guard` is guarding an /// existing usage of standard input. /// -/// Each file in `filenames` may be at most 16 MiB. +/// Each file in `filenames` may be at most 16 MiB. SSH keys are limited to 16 kiB. pub fn read_identities( filenames: Vec, max_work_factor: Option, @@ -125,7 +131,10 @@ pub(super) fn parse_identity_files + From>( // Try parsing as a single multi-line SSH identity. #[cfg(feature = "ssh")] - match crate::ssh::Identity::from_buffer(&mut reader, Some(filename.clone())) { + match crate::ssh::Identity::from_buffer( + LimitedReader::new(&mut reader, SSH_IDENTITY_SIZE_LIMIT), + Some(filename.clone()), + ) { Ok(crate::ssh::Identity::Unsupported(k)) => { return Err(ReadError::UnsupportedKey(filename, k).into()) } From e97d2d70684202b10796e80be61e859c59d91350 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Fri, 2 Jan 2026 23:04:22 +0000 Subject: [PATCH 17/20] CI: Switch to Go 1.24 for interop tests (cherry picked from commit bf4959c58994a2497e8a7d363fd5c03877691970) --- .github/workflows/interop.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/interop.yml b/.github/workflows/interop.yml index 24b6e81a..e1474920 100644 --- a/.github/workflows/interop.yml +++ b/.github/workflows/interop.yml @@ -48,10 +48,10 @@ jobs: -H 'Authorization: token ${{ secrets.AGE_STATUS_ACCESS_TOKEN }}' \ --data '{"state": "pending", "target_url": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}", "description": "In progress", "context": "Interoperability tests / Build age"}' - - name: Set up Go 1.19 + - name: Set up Go 1.24 uses: actions/setup-go@v5 with: - go-version: 1.19 + go-version: 1.24 id: go - name: Use specified FiloSottile/age commit From f45691042b3e0bcfa7bfbbb1517ad405a2b84145 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Wed, 22 Apr 2026 01:37:11 +0000 Subject: [PATCH 18/20] Reformat audit files to match `cargo-vet 0.10.2` format --- supply-chain/imports.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/supply-chain/imports.lock b/supply-chain/imports.lock index 12d32674..422c8d8f 100644 --- a/supply-chain/imports.lock +++ b/supply-chain/imports.lock @@ -223,7 +223,7 @@ who = "Nick Fitzgerald " criteria = "safe-to-deploy" user-id = 696 # Nick Fitzgerald (fitzgen) start = "2019-03-16" -end = "2025-07-30" +end = "2026-08-21" [[audits.bytecode-alliance.audits.addr2line]] who = "Alex Crichton " @@ -1033,8 +1033,8 @@ who = "Lukasz Anforowicz " criteria = "safe-to-deploy" version = "1.0.78" notes = """ -Grepped for \"crypt\", \"cipher\", \"fs\", \"net\" - there were no hits -(except for a benign \"fs\" hit in a doc comment) +Grepped for "crypt", "cipher", "fs", "net" - there were no hits +(except for a benign "fs" hit in a doc comment) Notes from the `unsafe` review can be found in https://crrev.com/c/5385745. """ @@ -1120,8 +1120,8 @@ who = "Lukasz Anforowicz " criteria = "safe-to-deploy" version = "1.0.35" notes = """ -Grepped for \"unsafe\", \"crypt\", \"cipher\", \"fs\", \"net\" - there were no hits -(except for benign \"net\" hit in tests and \"fs\" hit in README.md) +Grepped for "unsafe", "crypt", "cipher", "fs", "net" - there were no hits +(except for benign "net" hit in tests and "fs" hit in README.md) """ aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" @@ -1239,7 +1239,7 @@ aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_p who = "Lukasz Anforowicz " criteria = "safe-to-deploy" version = "1.0.197" -notes = "Grepped for \"unsafe\", \"crypt\", \"cipher\", \"fs\", \"net\" - there were no hits" +notes = 'Grepped for "unsafe", "crypt", "cipher", "fs", "net" - there were no hits' aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" [[audits.google.audits.serde_derive]] @@ -1258,7 +1258,7 @@ aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_p who = "Lukasz Anforowicz " criteria = "safe-to-deploy" delta = "1.0.202 -> 1.0.203" -notes = "Grepped for \"unsafe\", \"crypt\", \"cipher\", \"fs\", \"net\" - there were no hits" +notes = 'Grepped for "unsafe", "crypt", "cipher", "fs", "net" - there were no hits' aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" [[audits.google.audits.serde_derive]] From aa5b1d666932059780cd4df0a2077811e2d9350c Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Wed, 22 Apr 2026 01:37:38 +0000 Subject: [PATCH 19/20] cargo vet prune --- supply-chain/config.toml | 84 ---------------- supply-chain/imports.lock | 206 +++++++++++++++++++++++++++++++++----- 2 files changed, 181 insertions(+), 109 deletions(-) diff --git a/supply-chain/config.toml b/supply-chain/config.toml index f6b87255..8c28dda6 100644 --- a/supply-chain/config.toml +++ b/supply-chain/config.toml @@ -57,10 +57,6 @@ criteria = "safe-to-deploy" version = "1.1.1" criteria = "safe-to-deploy" -[[exemptions.android-tzdata]] -version = "0.1.1" -criteria = "safe-to-deploy" - [[exemptions.anstream]] version = "0.3.2" criteria = "safe-to-deploy" @@ -93,10 +89,6 @@ criteria = "safe-to-run" version = "1.6.0" criteria = "safe-to-deploy" -[[exemptions.basic-toml]] -version = "0.1.9" -criteria = "safe-to-deploy" - [[exemptions.bcrypt-pbkdf]] version = "0.10.0" criteria = "safe-to-deploy" @@ -145,18 +137,6 @@ criteria = "safe-to-deploy" version = "0.4.38" criteria = "safe-to-deploy" -[[exemptions.ciborium]] -version = "0.2.2" -criteria = "safe-to-run" - -[[exemptions.ciborium-io]] -version = "0.2.2" -criteria = "safe-to-run" - -[[exemptions.ciborium-ll]] -version = "0.2.2" -criteria = "safe-to-run" - [[exemptions.clap]] version = "4.3.24" criteria = "safe-to-deploy" @@ -253,14 +233,6 @@ criteria = "safe-to-deploy" version = "0.9.0" criteria = "safe-to-deploy" -[[exemptions.displaydoc]] -version = "0.2.5" -criteria = "safe-to-deploy" - -[[exemptions.dunce]] -version = "1.0.5" -criteria = "safe-to-run" - [[exemptions.encode_unicode]] version = "0.3.6" criteria = "safe-to-deploy" @@ -381,10 +353,6 @@ criteria = "safe-to-deploy" version = "0.8.4" criteria = "safe-to-deploy" -[[exemptions.iana-time-zone]] -version = "0.1.61" -criteria = "safe-to-deploy" - [[exemptions.indexmap]] version = "2.6.0" criteria = "safe-to-run" @@ -553,30 +521,14 @@ criteria = "safe-to-run" version = "0.2.20" criteria = "safe-to-deploy" -[[exemptions.proc-macro-error-attr2]] -version = "2.0.0" -criteria = "safe-to-deploy" - -[[exemptions.proc-macro-error2]] -version = "2.0.1" -criteria = "safe-to-deploy" - [[exemptions.proptest]] version = "1.5.0" criteria = "safe-to-run" -[[exemptions.quick-error]] -version = "1.2.3" -criteria = "safe-to-run" - [[exemptions.quick-xml]] version = "0.26.0" criteria = "safe-to-run" -[[exemptions.rand]] -version = "0.8.5" -criteria = "safe-to-deploy" - [[exemptions.redox_syscall]] version = "0.5.7" criteria = "safe-to-deploy" @@ -629,10 +581,6 @@ criteria = "safe-to-deploy" version = "0.38.34" criteria = "safe-to-deploy" -[[exemptions.rusty-fork]] -version = "0.3.0" -criteria = "safe-to-run" - [[exemptions.ryu]] version = "1.0.15" criteria = "safe-to-run" @@ -669,14 +617,6 @@ criteria = "safe-to-deploy" version = "0.6.3" criteria = "safe-to-run" -[[exemptions.sha2]] -version = "0.10.8" -criteria = "safe-to-deploy" - -[[exemptions.shlex]] -version = "1.3.0" -criteria = "safe-to-deploy" - [[exemptions.similar]] version = "2.6.0" criteria = "safe-to-run" @@ -685,10 +625,6 @@ criteria = "safe-to-run" version = "0.4.9" criteria = "safe-to-deploy" -[[exemptions.smallvec]] -version = "1.11.1" -criteria = "safe-to-deploy" - [[exemptions.snapbox]] version = "0.4.11" criteria = "safe-to-run" @@ -781,14 +717,6 @@ criteria = "safe-to-deploy" version = "1.15.0" criteria = "safe-to-deploy" -[[exemptions.unarray]] -version = "0.1.4" -criteria = "safe-to-run" - -[[exemptions.utf8parse]] -version = "0.2.2" -criteria = "safe-to-deploy" - [[exemptions.uuid]] version = "1.11.0" criteria = "safe-to-run" @@ -841,10 +769,6 @@ criteria = "safe-to-deploy" version = "0.4.0" criteria = "safe-to-deploy" -[[exemptions.windows-core]] -version = "0.52.0" -criteria = "safe-to-deploy" - [[exemptions.windows_i686_gnullvm]] version = "0.52.6" criteria = "safe-to-deploy" @@ -877,14 +801,6 @@ criteria = "safe-to-deploy" version = "0.7.35" criteria = "safe-to-deploy" -[[exemptions.zeroize]] -version = "1.8.1" -criteria = "safe-to-deploy" - -[[exemptions.zeroize_derive]] -version = "1.3.2" -criteria = "safe-to-deploy" - [[exemptions.zip]] version = "0.6.6" criteria = "safe-to-deploy" diff --git a/supply-chain/imports.lock b/supply-chain/imports.lock index 422c8d8f..fd74ebea 100644 --- a/supply-chain/imports.lock +++ b/supply-chain/imports.lock @@ -522,6 +522,12 @@ criteria = "safe-to-deploy" delta = "0.10.5 -> 0.10.6" notes = "Only new code is some loongarch64 additions which include assembly code for that platform." +[[audits.bytecode-alliance.audits.shlex]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +version = "1.1.0" +notes = "Only minor `unsafe` code blocks which look valid and otherwise does what it says on the tin." + [[audits.bytecode-alliance.audits.tempfile]] who = "Pat Hickey " criteria = "safe-to-deploy" @@ -533,6 +539,15 @@ criteria = "safe-to-deploy" delta = "3.5.0 -> 3.6.0" notes = "Dependency updates and new optimized trait implementations, but otherwise everything looks normal." +[[audits.bytecode-alliance.audits.unarray]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +version = "0.1.4" +notes = """ +Crate is sound, albeit leaky, and not actively malicious. Probably not the best +crate to use in practice but it's suitable for testing dependencies. +""" + [[audits.bytecode-alliance.audits.xattr]] who = "Andrew Brown " criteria = "safe-to-deploy" @@ -563,6 +578,12 @@ criteria = "safe-to-deploy" delta = "0.6.1 -> 0.6.2" notes = "No notable changes" +[[audits.embark-studios.audits.utf8parse]] +who = "Johan Andersson " +criteria = "safe-to-deploy" +version = "0.2.1" +notes = "Single unsafe usage that looks sound, no ambient capabilities" + [[audits.fermyon.audits.oorandom]] who = "Radu Matei " criteria = "safe-to-run" @@ -722,6 +743,24 @@ criteria = "safe-to-run" version = "0.3.0" aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" +[[audits.google.audits.ciborium]] +who = "Daniel Verkamp " +criteria = "safe-to-run" +version = "0.2.2" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + +[[audits.google.audits.ciborium-io]] +who = "Daniel Verkamp " +criteria = "safe-to-run" +version = "0.2.2" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + +[[audits.google.audits.ciborium-ll]] +who = "Daniel Verkamp " +criteria = "safe-to-run" +version = "0.2.2" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + [[audits.google.audits.cpp_demangle]] who = "Hidenori Kobayashi " criteria = "safe-to-run" @@ -758,6 +797,13 @@ criteria = "safe-to-run" delta = "0.9.14 -> 0.9.15" aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" +[[audits.google.audits.displaydoc]] +who = "Manish Goregaokar " +criteria = "safe-to-deploy" +version = "0.2.5" +notes = "No unsafe code" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + [[audits.google.audits.equivalent]] who = "George Burgess IV " criteria = "safe-to-deploy" @@ -883,6 +929,13 @@ criteria = "safe-to-deploy" version = "0.3.1" aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" +[[audits.google.audits.iana-time-zone]] +who = "Manish Goregaokar " +criteria = "safe-to-deploy" +version = "0.1.61" +notes = "Some unsafe: interfacing with system timezone APIs" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + [[audits.google.audits.itertools]] who = "ChromeOS" criteria = "safe-to-run" @@ -1115,6 +1168,12 @@ Some config related changes in wrapper.rs. """ aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +[[audits.google.audits.quick-error]] +who = "George Burgess IV " +criteria = "safe-to-run" +version = "1.2.3" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + [[audits.google.audits.quote]] who = "Lukasz Anforowicz " criteria = "safe-to-deploy" @@ -1141,6 +1200,21 @@ The delta just 1) inlines/expands `impl ToTokens` that used to be handled via """ aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +[[audits.google.audits.rand]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +version = "0.8.5" +notes = """ +For more detailed unsafe review notes please see https://crrev.com/c/6362797 +""" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.rusty-fork]] +who = "George Burgess IV " +criteria = "safe-to-run" +version = "0.3.0" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + [[audits.google.audits.serde]] who = "Lukasz Anforowicz " criteria = "safe-to-deploy" @@ -1372,6 +1446,12 @@ version = "0.10.5" notes = "Reviewed on https://fxrev.dev/712371." aggregated-from = "https://fuchsia.googlesource.com/fuchsia/+/refs/heads/main/third_party/rust_crates/supply-chain/audits.toml?format=TEXT" +[[audits.google.audits.smallvec]] +who = "Manish Goregaokar " +criteria = "safe-to-deploy" +version = "1.13.2" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + [[audits.google.audits.stable_deref_trait]] who = "George Burgess IV " criteria = "safe-to-run" @@ -1424,6 +1504,13 @@ criteria = "safe-to-run" version = "0.2.0" aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" +[[audits.google.audits.windows-core]] +who = "Manish Goregaokar " +criteria = "safe-to-deploy" +version = "0.52.0" +notes = "Implements Windows system APIs" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + [[audits.isrg.audits.base64]] who = "Tim Geoghegan " criteria = "safe-to-deploy" @@ -1659,6 +1746,11 @@ who = "Ameer Ghani " criteria = "safe-to-deploy" version = "1.12.1" +[[audits.isrg.audits.sha2]] +who = "David Cook " +criteria = "safe-to-deploy" +version = "0.10.2" + [[audits.isrg.audits.subtle]] who = "David Cook " criteria = "safe-to-deploy" @@ -1709,6 +1801,13 @@ renew = false notes = "I've reviewed every source contribution that was neither authored nor reviewed by Mozilla." aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" +[[audits.mozilla.audits.android-tzdata]] +who = "Mark Hammond " +criteria = "safe-to-deploy" +version = "0.1.1" +notes = "Small crate parsing a file. No unsafe code" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + [[audits.mozilla.audits.android_system_properties]] who = "Nicolas Silva " criteria = "safe-to-deploy" @@ -1735,6 +1834,19 @@ delta = "0.7.2 -> 0.7.6" notes = "Manually verified new unsafe pointer arithmetic." aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" +[[audits.mozilla.audits.basic-toml]] +who = "Jan-Erik Rediger " +criteria = "safe-to-deploy" +version = "0.1.2" +notes = "TOML parser, forked from toml 0.5" +aggregated-from = "https://raw.githubusercontent.com/mozilla/glean/main/supply-chain/audits.toml" + +[[audits.mozilla.audits.basic-toml]] +who = "Jan-Erik Rediger " +criteria = "safe-to-deploy" +delta = "0.1.2 -> 0.1.9" +aggregated-from = "https://raw.githubusercontent.com/mozilla/glean/main/supply-chain/audits.toml" + [[audits.mozilla.audits.bit-set]] who = "Aria Beingessner " criteria = "safe-to-deploy" @@ -2024,6 +2136,20 @@ criteria = "safe-to-deploy" delta = "0.3.25 -> 0.3.26" aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" +[[audits.mozilla.audits.proc-macro-error-attr2]] +who = "Kagami Sascha Rosylight " +criteria = "safe-to-deploy" +version = "2.0.0" +notes = "No unsafe block." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.proc-macro-error2]] +who = "Kagami Sascha Rosylight " +criteria = "safe-to-deploy" +version = "2.0.1" +notes = "No unsafe block with a lovely `#![forbid(unsafe_code)]`." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + [[audits.mozilla.audits.rand_core]] who = "Mike Hommey " criteria = "safe-to-deploy" @@ -2050,6 +2176,29 @@ version = "1.1.0" notes = "Straightforward crate with no unsafe code, does what it says on the tin." aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" +[[audits.mozilla.audits.sha2]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.10.2 -> 0.10.6" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.sha2]] +who = "Jeff Muizelaar " +criteria = "safe-to-deploy" +delta = "0.10.6 -> 0.10.8" +notes = """ +The bulk of this is https://github.com/RustCrypto/hashes/pull/490 which adds aarch64 support along with another PR adding longson. +I didn't check the implementation thoroughly but there wasn't anything obviously nefarious. 0.10.8 has been out for more than a year +which suggests no one else has found anything either. +""" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.shlex]] +who = "Max Inden " +criteria = "safe-to-deploy" +delta = "1.1.0 -> 1.3.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + [[audits.mozilla.audits.strsim]] who = "Ben Dean-Kawamura " criteria = "safe-to-deploy" @@ -2172,6 +2321,28 @@ criteria = "safe-to-deploy" delta = "0.9.1 -> 0.9.5" aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" +[[audits.mozilla.audits.utf8parse]] +who = "Nika Layzell " +criteria = "safe-to-deploy" +delta = "0.2.1 -> 0.2.2" +aggregated-from = "https://raw.githubusercontent.com/mozilla/cargo-vet/main/supply-chain/audits.toml" + +[[audits.mozilla.audits.zeroize]] +who = "Benjamin Beurdouche " +criteria = "safe-to-deploy" +version = "1.8.1" +notes = """ +This code DOES contain unsafe code required to internally call volatiles +for deleting data. This is expected and documented behavior. +""" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.zeroize_derive]] +who = "Benjamin Beurdouche " +criteria = "safe-to-deploy" +version = "1.4.2" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + [[audits.zcash.audits.aead]] who = "Jack Grigg " criteria = "safe-to-deploy" @@ -2317,6 +2488,16 @@ delta = "0.7.8 -> 0.7.9" notes = "The change to ignore RUSTSEC-2023-0071 is correct for this crate." aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" +[[audits.zcash.audits.dunce]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +version = "1.0.5" +notes = """ +Does what it says on the tin. No `unsafe`, and the only IO is `std::fs::canonicalize`. +Path and string handling looks plausibly correct. +""" +aggregated-from = "https://raw.githubusercontent.com/zcash/librustzcash/main/supply-chain/audits.toml" + [[audits.zcash.audits.either]] who = "Jack Grigg " criteria = "safe-to-deploy" @@ -2790,12 +2971,6 @@ criteria = "safe-to-deploy" delta = "2.1.0 -> 2.2.0" aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" -[[audits.zcash.audits.smallvec]] -who = "Daira-Emma Hopwood " -criteria = "safe-to-deploy" -delta = "1.11.1 -> 1.13.2" -aggregated-from = "https://raw.githubusercontent.com/zcash/librustzcash/main/supply-chain/audits.toml" - [[audits.zcash.audits.thiserror]] who = "Jack Grigg " criteria = "safe-to-deploy" @@ -3020,22 +3195,3 @@ Crate now has `#![forbid(unsafe_code)]`, replacing its last `unsafe` block with dependency on the `rustix` crate. """ aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" - -[[audits.zcash.audits.zeroize_derive]] -who = "Jack Grigg " -criteria = "safe-to-deploy" -delta = "1.3.2 -> 1.3.3" -notes = "Removes `T: Drop` bound from `impl Drop for SomeType`. I agree it was unnecessary." -aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" - -[[audits.zcash.audits.zeroize_derive]] -who = "Sean Bowe " -criteria = "safe-to-deploy" -delta = "1.3.3 -> 1.4.1" -aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" - -[[audits.zcash.audits.zeroize_derive]] -who = "Jack Grigg " -criteria = "safe-to-deploy" -delta = "1.4.1 -> 1.4.2" -aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" From 681faceaf70973a691cee594f3ab0944cf0065e5 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Wed, 22 Apr 2026 01:54:52 +0000 Subject: [PATCH 20/20] age 0.11.3 --- Cargo.lock | 2 +- age/CHANGELOG.md | 2 ++ age/Cargo.toml | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7faabc9a..eb081d7f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -60,7 +60,7 @@ dependencies = [ [[package]] name = "age" -version = "0.11.2" +version = "0.11.3" dependencies = [ "aes", "aes-gcm", diff --git a/age/CHANGELOG.md b/age/CHANGELOG.md index 06715f88..e725ec1e 100644 --- a/age/CHANGELOG.md +++ b/age/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to Rust's notion of to 1.0.0 are beta releases. ## [Unreleased] + +## [0.11.3] - 2026-04-22 ### Changed - Recipient and identity files (parsed via `age::IdentityFile` or `age::cli_common::{read_recipients, read_identities}`) is now limited to at diff --git a/age/Cargo.toml b/age/Cargo.toml index 994ca3f5..a3f7312b 100644 --- a/age/Cargo.toml +++ b/age/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "age" description = "[BETA] A simple, secure, and modern encryption library." -version = "0.11.2" +version = "0.11.3" authors.workspace = true repository.workspace = true readme = "README.md"