Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 27 additions & 21 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,15 @@ jobs:

include:
- name: linux
os: ubuntu-20.04
os: ubuntu-22.04
build_deps: >
libfuse-dev
build_flags: --features mount
archive_name: rage.tar.gz
asset_suffix: x86_64-linux.tar.gz

- name: armv7
os: ubuntu-20.04
os: ubuntu-22.04
target: armv7-unknown-linux-gnueabihf
build_deps: >
gcc-arm-linux-gnueabihf
Expand All @@ -52,7 +52,7 @@ jobs:
asset_suffix: armv7-linux.tar.gz

- name: arm64
os: ubuntu-20.04
os: ubuntu-22.04
target: aarch64-unknown-linux-gnu
build_deps: >
gcc-aarch64-linux-gnu
Expand All @@ -74,7 +74,7 @@ jobs:
asset_suffix: arm64-darwin.tar.gz

- name: macos-x86_64
os: macos-13
os: macos-15-intel
archive_name: rage.tar.gz
asset_suffix: x86_64-darwin.tar.gz

Expand Down Expand Up @@ -149,21 +149,17 @@ jobs:
strategy:
matrix:
os:
- ubuntu-20.04
- ubuntu-22.04
- ubuntu-24.04
- windows-2019
- windows-2022
- macos-12
- macos-13
- windows-2025
- macos-14
- macos-15
- macos-15-intel
- macos-26
- macos-26-intel

include:
- os: ubuntu-20.04
name: linux
archive_name: rage.tar.gz
asset_suffix: x86_64-linux.tar.gz

- os: ubuntu-22.04
name: linux
archive_name: rage.tar.gz
Expand All @@ -174,31 +170,41 @@ jobs:
archive_name: rage.tar.gz
asset_suffix: x86_64-linux.tar.gz

- os: windows-2019
- os: windows-2022
name: windows
archive_name: rage.zip
asset_suffix: x86_64-windows.zip

- os: windows-2022
- os: windows-2025
name: windows
archive_name: rage.zip
asset_suffix: x86_64-windows.zip

- os: macos-12
- os: macos-14
name: macos
archive_name: rage.tar.gz
asset_suffix: x86_64-darwin.tar.gz
asset_suffix: arm64-darwin.tar.gz

- os: macos-13
- os: macos-15
name: macos
archive_name: rage.tar.gz
asset_suffix: arm64-darwin.tar.gz

- os: macos-15-intel
name: macos
archive_name: rage.tar.gz
asset_suffix: x86_64-darwin.tar.gz

- os: macos-14
- os: macos-26
name: macos
archive_name: rage.tar.gz
asset_suffix: arm64-darwin.tar.gz

- os: macos-26-intel
name: macos
archive_name: rage.tar.gz
asset_suffix: x86_64-darwin.tar.gz

steps:
- name: Download archive
uses: actions/download-artifact@v4
Expand All @@ -225,7 +231,7 @@ jobs:

deb:
name: Debian ${{ matrix.name }}
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
strategy:
matrix:
name: [linux, linux-musl, armv7, armv7-musl, arm64, arm64-musl]
Expand Down Expand Up @@ -338,7 +344,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-20.04, ubuntu-22.04]
os: [ubuntu-22.04, ubuntu-24.04]
variant: [linux, linux-musl]

steps:
Expand Down
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ For more plugins, implementations, tools, and integrations, check out the
| Debian | [Debian packages](https://github.com/str4d/rage/releases) |
| NixOS | Add to config: `environment.systemPackages = [ pkgs.rage ];`<br>Or run `nix-env -i rage` |
| openSUSE Tumbleweed | `zypper install rage-encryption` |
| Ubuntu 20.04+ | [Debian packages](https://github.com/str4d/rage/releases) |
| Ubuntu 22.04+ | [Debian packages](https://github.com/str4d/rage/releases) |
| FreeBSD | `pkg install rage-encryption` |
| Scoop (Windows) | `scoop bucket add main`<br>`scoop install main/rage` |

Expand Down
20 changes: 20 additions & 0 deletions age/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,26 @@ to 1.0.0 are beta releases.
- `age::EncryptError::MissingPlugin`
- `age::cli_common::ReadError::MissingPlugin`

## [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
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`:
- `{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
ciphertext truncated to just after the nonce (i.e. with zero chunk data).

## [0.11.2] - 2025-12-07
### Fixed
- `age::armor::ArmoredWriter::poll_write` no longer panics when writing more
Expand Down
2 changes: 1 addition & 1 deletion age/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
13 changes: 12 additions & 1 deletion age/src/cli_common/identities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +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. SSH keys are limited to 16 kiB.
pub fn read_identities(
filenames: Vec<String>,
max_work_factor: Option<u8>,
Expand Down Expand Up @@ -121,7 +129,10 @@ pub(super) fn parse_identity_files<Ctx, E: From<ReadError> + From<io::Error>>(

// 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())
}
Expand Down
8 changes: 6 additions & 2 deletions age/src/cli_common/recipients.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::io::{self, BufReader};

use super::StdinGuard;
use super::{identities::parse_identity_files, ReadError};
use crate::{identity::RecipientsAccumulator, tag, tagpq, x25519, Recipient};
use crate::{identity::RecipientsAccumulator, tag, tagpq, util::LimitedReader, x25519, Recipient};

#[cfg(feature = "plugin")]
use crate::{cli_common::UiCallbacks, plugin};
Expand All @@ -16,6 +16,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
Expand Down Expand Up @@ -129,6 +131,8 @@ fn read_recipients_list<R: io::BufRead>(
/// `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<String>,
recipients_file_strings: Vec<String>,
Expand All @@ -149,7 +153,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)?;
}

Expand Down
11 changes: 9 additions & 2 deletions age/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
#[non_exhaustive]
Expand Down Expand Up @@ -111,8 +114,12 @@ pub enum PluginError {
#[cfg(feature = "plugin")]
impl From<Stanza> 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,
Expand Down
25 changes: 21 additions & 4 deletions age/src/identity.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
use std::fs::File;
use std::io;
use std::{
fs::File,
io::{self, BufRead},
};

use crate::{x25519, Callbacks, DecryptError, EncryptError, IdentityFileConvertError, NoCallbacks};
use zeroize::Zeroize;

use crate::{
util::LimitedReader, x25519, Callbacks, DecryptError, EncryptError, IdentityFileConvertError,
NoCallbacks,
};

#[cfg(feature = "cli-common")]
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 {
Expand Down Expand Up @@ -41,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<C: Callbacks> {
filename: Option<String>,
identities: Vec<IdentityFileEntry>,
Expand Down Expand Up @@ -70,8 +81,10 @@ impl IdentityFile<NoCallbacks> {
fn parse_identities<R: io::BufRead>(filename: Option<String>, data: R) -> io::Result<Self> {
let mut identities = vec![];

let data = LimitedReader::new(data, IDENTITY_SIZE_LIMIT);

for (line_number, line) in data.lines().enumerate() {
let line = line?;
let mut line = line?;
if line.is_empty() || line.starts_with('#') {
continue;
}
Expand All @@ -96,6 +109,8 @@ impl IdentityFile<NoCallbacks> {
#[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(
Expand All @@ -114,6 +129,8 @@ impl IdentityFile<NoCallbacks> {
},
));
}

line.zeroize();
}

Ok(IdentityFile {
Expand Down
7 changes: 7 additions & 0 deletions age/src/native/scrypt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
10 changes: 8 additions & 2 deletions age/src/native/x25519.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,16 @@ impl std::str::FromStr for Identity {
.ok_or("incorrect HRP")
},
|_, bytes| {
TryInto::<[u8; 32]>::try_into(bytes.collect::<Vec<_>>())
let mut buf = bytes.collect::<Vec<_>>();
let identity = TryInto::<[u8; 32]>::try_into(buf.as_slice())
.map_err(|_| "incorrect identity length")
.map(StaticSecret::from)
.map(Identity)
.map(Identity);

// Clear intermediates
buf.zeroize();

identity
},
)
}
Expand Down
Loading
Loading