Skip to content

Commit 75731ce

Browse files
authored
Merge pull request #589 from str4d/age-0.11.2
`age 0.11.2`
2 parents d20c886 + 88bf527 commit 75731ce

File tree

6 files changed

+157
-15
lines changed

6 files changed

+157
-15
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

age/CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,15 @@ to 1.0.0 are beta releases.
2121
cross-thread uses of `IdentityFile`, which were unintentionally disabled in
2222
0.11.0.
2323

24+
## [0.11.2] - 2025-12-07
25+
### Fixed
26+
- `age::armor::ArmoredWriter::poll_write` no longer panics when writing more
27+
than 6144 bytes.
28+
- `age::encrypted::Identity` no longer causes a panic when being decrypted if
29+
the `age::Callbacks::request_passphrase` impl returns `None`.
30+
- `age::plugin::{Identity, RecipientPluginV1, IdentityPluginV1}` now correctly
31+
reject the empty plugin name (like `age::plugin::Recipient` already was).
32+
2433
## [0.6.1, 0.7.2, 0.8.2, 0.9.3, 0.10.1, 0.11.1] - 2024-11-18
2534
### Security
2635
- Fixed a security vulnerability that could allow an attacker to execute an

age/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "age"
33
description = "[BETA] A simple, secure, and modern encryption library."
4-
version = "0.11.1"
4+
version = "0.11.2"
55
authors.workspace = true
66
repository.workspace = true
77
readme = "README.md"

age/src/encrypted.rs

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ impl<R: io::Read, C: Callbacks> EncryptedIdentity<R, C> {
5050
filename = filename.unwrap_or_default()
5151
)) {
5252
Some(passphrase) => passphrase,
53-
None => todo!(),
53+
None => Err(DecryptError::KeyDecryptionFailed)?,
5454
};
5555

5656
let mut identity = scrypt::Identity::new(passphrase);
@@ -251,10 +251,10 @@ fOrxrKTj7xCdNS3+OrCdnBC8Z9cKDxjCGWW3fkjLsYha0Jo=
251251
const TEST_RECIPIENT: &str = "age1ysxuaeqlk7xd8uqsh8lsnfwt9jzzjlqf49ruhpjrrj5yatlcuf7qke4pqe";
252252

253253
#[derive(Clone)]
254-
struct MockCallbacks(Arc<Mutex<Option<&'static str>>>);
254+
struct MockCallbacks(Arc<Mutex<Option<Option<&'static str>>>>);
255255

256256
impl MockCallbacks {
257-
fn new(passphrase: &'static str) -> Self {
257+
fn new(passphrase: Option<&'static str>) -> Self {
258258
MockCallbacks(Arc::new(Mutex::new(Some(passphrase))))
259259
}
260260
}
@@ -274,9 +274,13 @@ fOrxrKTj7xCdNS3+OrCdnBC8Z9cKDxjCGWW3fkjLsYha0Jo=
274274

275275
/// This intentionally panics if called twice.
276276
fn request_passphrase(&self, _: &str) -> Option<SecretString> {
277-
Some(SecretString::from(
278-
self.0.lock().unwrap().take().unwrap().to_owned(),
279-
))
277+
self.0
278+
.lock()
279+
.unwrap()
280+
.take()
281+
.expect("passphrase is only input once")
282+
.to_owned()
283+
.map(SecretString::from)
280284
}
281285
}
282286

@@ -293,10 +297,28 @@ fOrxrKTj7xCdNS3+OrCdnBC8Z9cKDxjCGWW3fkjLsYha0Jo=
293297
// Unwrapping with the wrong passphrase fails.
294298
{
295299
let buf = ArmoredReader::new(TEST_ENCRYPTED_IDENTITY.as_bytes());
296-
let identity =
297-
Identity::from_buffer(buf, None, MockCallbacks::new("wrong passphrase"), None)
298-
.unwrap()
299-
.unwrap();
300+
let identity = Identity::from_buffer(
301+
buf,
302+
None,
303+
MockCallbacks::new(Some("wrong passphrase")),
304+
None,
305+
)
306+
.unwrap()
307+
.unwrap();
308+
309+
if let Err(e) = identity.unwrap_stanzas(&wrapped).unwrap() {
310+
assert!(matches!(e, DecryptError::KeyDecryptionFailed));
311+
} else {
312+
panic!("Should have failed");
313+
}
314+
}
315+
316+
// Unwrapping fails if we cannot obtain a passphrase.
317+
{
318+
let buf = ArmoredReader::new(TEST_ENCRYPTED_IDENTITY.as_bytes());
319+
let identity = Identity::from_buffer(buf, None, MockCallbacks::new(None), None)
320+
.unwrap()
321+
.unwrap();
300322

301323
if let Err(e) = identity.unwrap_stanzas(&wrapped).unwrap() {
302324
assert!(matches!(e, DecryptError::KeyDecryptionFailed));
@@ -309,7 +331,7 @@ fOrxrKTj7xCdNS3+OrCdnBC8Z9cKDxjCGWW3fkjLsYha0Jo=
309331
let identity = Identity::from_buffer(
310332
buf,
311333
None,
312-
MockCallbacks::new(TEST_ENCRYPTED_IDENTITY_PASSPHRASE),
334+
MockCallbacks::new(Some(TEST_ENCRYPTED_IDENTITY_PASSPHRASE)),
313335
None,
314336
)
315337
.unwrap()

age/src/plugin.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ fn valid_plugin_name(plugin_name: &str) -> bool {
4848
plugin_name
4949
.bytes()
5050
.all(|b| b.is_ascii_alphanumeric() | matches!(b, b'+' | b'-' | b'.' | b'_'))
51+
&& !plugin_name.is_empty()
5152
}
5253

5354
fn binary_name(plugin_name: &str) -> String {
@@ -753,6 +754,13 @@ mod tests {
753754
);
754755
}
755756

757+
#[test]
758+
fn recipient_rejects_empty_name() {
759+
let invalid_recipient =
760+
bech32::encode(PLUGIN_RECIPIENT_PREFIX, [], bech32::Variant::Bech32).unwrap();
761+
assert!(invalid_recipient.parse::<Recipient>().is_err());
762+
}
763+
756764
#[test]
757765
fn recipient_rejects_invalid_chars() {
758766
let invalid_recipient = bech32::encode(
@@ -764,6 +772,18 @@ mod tests {
764772
assert!(invalid_recipient.parse::<Recipient>().is_err());
765773
}
766774

775+
#[test]
776+
fn identity_rejects_empty_name() {
777+
let invalid_identity = bech32::encode(
778+
&format!("{}-", PLUGIN_IDENTITY_PREFIX),
779+
[],
780+
bech32::Variant::Bech32,
781+
)
782+
.expect("HRP is valid")
783+
.to_uppercase();
784+
assert!(invalid_identity.parse::<Identity>().is_err());
785+
}
786+
767787
#[test]
768788
fn identity_rejects_invalid_chars() {
769789
let invalid_identity = bech32::encode(
@@ -776,12 +796,26 @@ mod tests {
776796
assert!(invalid_identity.parse::<Identity>().is_err());
777797
}
778798

799+
#[test]
800+
#[should_panic]
801+
fn identity_default_for_plugin_rejects_empty_name() {
802+
Identity::default_for_plugin("");
803+
}
804+
779805
#[test]
780806
#[should_panic]
781807
fn identity_default_for_plugin_rejects_invalid_chars() {
782808
Identity::default_for_plugin(INVALID_PLUGIN_NAME);
783809
}
784810

811+
#[test]
812+
fn recipient_plugin_v1_rejects_empty_name() {
813+
assert!(matches!(
814+
RecipientPluginV1::new("", &[], &[], NoCallbacks),
815+
Err(EncryptError::MissingPlugin { binary_name }) if binary_name.is_empty(),
816+
));
817+
}
818+
785819
#[test]
786820
fn recipient_plugin_v1_rejects_invalid_chars() {
787821
assert!(matches!(
@@ -790,6 +824,14 @@ mod tests {
790824
));
791825
}
792826

827+
#[test]
828+
fn identity_plugin_v1_rejects_empty_name() {
829+
assert!(matches!(
830+
IdentityPluginV1::new("", &[], NoCallbacks),
831+
Err(DecryptError::MissingPlugin { binary_name }) if binary_name.is_empty(),
832+
));
833+
}
834+
793835
#[test]
794836
fn identity_plugin_v1_rejects_invalid_chars() {
795837
assert!(matches!(

age/src/primitives/armor.rs

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -517,11 +517,11 @@ impl<W: AsyncWrite> AsyncWrite for ArmoredWriter<W> {
517517
BASE64_STANDARD
518518
.encode_slice(&byte_buf, &mut encoded_buf[..],)
519519
.expect("byte_buf.len() <= BASE64_CHUNK_SIZE_BYTES"),
520-
ARMORED_COLUMNS_PER_LINE
520+
BASE64_CHUNK_SIZE_COLUMNS
521521
);
522522
*encoded_line = Some(EncodedBytes {
523523
offset: 0,
524-
end: ARMORED_COLUMNS_PER_LINE,
524+
end: BASE64_CHUNK_SIZE_COLUMNS,
525525
});
526526
byte_buf.clear();
527527
}
@@ -1523,4 +1523,73 @@ mod tests {
15231523
r.read_exact(&mut buf).unwrap();
15241524
assert_eq!(&buf[..], &data[data.len() - 1337..data.len() - 1237]);
15251525
}
1526+
1527+
#[cfg(feature = "async")]
1528+
#[test]
1529+
fn armored_async_cross_check() {
1530+
let data =
1531+
vec![42; (super::BASE64_CHUNK_SIZE_BYTES * 2) + (super::ARMORED_BYTES_PER_LINE * 10)];
1532+
1533+
let mut encoded_sync = vec![];
1534+
{
1535+
let mut out =
1536+
ArmoredWriter::wrap_output(&mut encoded_sync, Format::AsciiArmor).unwrap();
1537+
out.write_all(&data).unwrap();
1538+
out.finish().unwrap();
1539+
}
1540+
1541+
let mut encoded_async = vec![];
1542+
{
1543+
let w = ArmoredWriter::wrap_async_output(&mut encoded_async, Format::AsciiArmor);
1544+
pin_mut!(w);
1545+
1546+
let mut cx = noop_context();
1547+
1548+
let mut tmp = &data[..];
1549+
loop {
1550+
match w.as_mut().poll_write(&mut cx, tmp) {
1551+
Poll::Ready(Ok(0)) => break,
1552+
Poll::Ready(Ok(written)) => tmp = &tmp[written..],
1553+
Poll::Ready(Err(e)) => panic!("Unexpected error: {}", e),
1554+
Poll::Pending => panic!("Unexpected Pending"),
1555+
}
1556+
}
1557+
loop {
1558+
match w.as_mut().poll_close(&mut cx) {
1559+
Poll::Ready(Ok(())) => break,
1560+
Poll::Ready(Err(e)) => panic!("Unexpected error: {}", e),
1561+
Poll::Pending => panic!("Unexpected Pending"),
1562+
}
1563+
}
1564+
}
1565+
1566+
assert_eq!(encoded_sync, encoded_async);
1567+
1568+
let mut buf_sync = vec![];
1569+
{
1570+
let mut input = ArmoredReader::new(&encoded_sync[..]);
1571+
input.read_to_end(&mut buf_sync).unwrap();
1572+
}
1573+
1574+
let mut buf_async = vec![];
1575+
{
1576+
let input = ArmoredReader::from_async_reader(&encoded_async[..]);
1577+
pin_mut!(input);
1578+
1579+
let mut cx = noop_context();
1580+
1581+
let mut tmp = [0; 4096];
1582+
loop {
1583+
match input.as_mut().poll_read(&mut cx, &mut tmp) {
1584+
Poll::Ready(Ok(0)) => break,
1585+
Poll::Ready(Ok(read)) => buf_async.extend_from_slice(&tmp[..read]),
1586+
Poll::Ready(Err(e)) => panic!("Unexpected error: {}", e),
1587+
Poll::Pending => panic!("Unexpected Pending"),
1588+
}
1589+
}
1590+
}
1591+
1592+
assert_eq!(buf_async, data);
1593+
assert_eq!(buf_sync, data);
1594+
}
15261595
}

0 commit comments

Comments
 (0)