A simple, secure and modern file encryption tool (and Rust library) with small explicit keys, no config options, and UNIX-style composability.
rage-keygen -y IDENTITY_FILE
to convert identity files to recipients.gumdrop
to clap
for argument parsing.-R/--recipients-file
and -i/--identity
now support "read-once" files, like those used by process substitution (-i <(other_binary get-age-identity)
) and named pipes.-
(hyphen) is now treated as an explicit request to read from standard input when used with -R/--recipients-file
or -i/--identity
. It must only occur once across the -R/--recipients-file
and -i/--identity
flags, and the input file. It cannot be used if the input file is omitted.-i/--identity
that contain invalid public keys are no longer ignored when encrypting, and instead cause an error.ssh-rsa
public keys that are smaller than 2048 bits are now rejected.rage-keygen
no longer overwrites existing key files with the -o/--output
flag. This was its behaviour prior to 0.6.0, but was unintentionally changed when rage
was modified to overwrite existing files. Key file overwriting can still be achieved by omitting -o/--output
and instead piping stdout to the file.rage-keygen
now prints fatal errors directly instead of them being hidden behind the RUST_LOG=error
environment variable. It also now sets its return code appropriately instead of always returning 0.age::cli_common
:
file_io
:
FileReader
impl Debug for {LazyFile, OutputFormat, OutputWriter, StdoutWriter}
StdinGuard
read_recipients
age::identity::IdentityFile::from_input_reader
(behind cli-common
feature flag).impl Eq for age::ssh::{ParseRecipientKeyError, UnsupportedKey}
impl {Debug, PartialEq, Eq, Hash} for age::x25519::Recipient
base64 0.21
, rsa 0.9
.age::cli_common
:
file_io
:
InputReader::File
enum variant now contains FileReader
instead of std::fs::File
.OutputWriter::new
now takes an allow_overwrite
boolean argument. If OutputWriter
will write to a file, this boolean enables the caller to control whether the file will be overwritten if it exists (instead of the implicit behaviour that was previously changed in 0.6.0).read_identities
now takes an &mut StdinGuard
argument, and filenames
may now contain at most one entry of "-"
, which will be interpreted as reading from standard input.ReadError
has new variants:
EncryptedIdentities
InvalidRecipient
InvalidRecipientsFile
MissingRecipientsFile
MultipleStdin
RsaModulusTooLarge
RsaModulusTooSmall
age::ssh
:
ParseRecipientKeyError
has new variants:
RsaModulusTooLarge
RsaModulusTooSmall
Err(ParseRecipientKeyError::RsaModulusTooLarge)
instead of Err(ParseRecipientKeyError::Unsupported(_))
when encountering an RSA public key with a modulus larger than 4096 bits:
impl FromStr for Recipient
impl TryFrom<Identity> for Recipient
age::Encryptor::with_user_passphrase
will now re-measure the scrypt
work factor until it is measurable, instead of setting the work factor to maximum.age::cli_common
:
UiCallbacks::confirm
no longer requires erasing the confirmation message before it will accept a response.UiCallbacks::request_public_string
no longer prepends the description to the response string.ssh-rsa
public keys that are smaller than 2048 bits are now rejected from all string-parsing APIs. The Recipient::SshRsa
enum variant can still be manually constructed with such keys; this will be fixed in a future crate refactor.age-core 0.10
.age_plugin::run_state_machine
now takes optional arguments, to enable the creation of recipient-only or identity-only plugins.impl Eq for age_core::format::Stanza
Full Changelog: https://github.com/str4d/rage/compare/v0.9.2...v0.10.0
pinentry
binary used to request passphrases can now be set manually with PINENTRY_PROGRAM
environment variable. It accepts either a binary name a path. Setting this to the empty string will disable pinentry
usage and back to the CLI interface.age::Decryptor::{new_buffered, new_async_buffered}
, which are more efficient types implementing std::io::BufRead
or futures::io::AsyncBufRead
includes &[u8]
slices).impl std::io::BufRead for age::armor::ArmoredReader
impl futures::io::AsyncBufRead for age::armor::ArmoredReader
pinentry
binary used by age::cli_common::read_secret
can now be set with the PINENTRY_PROGRAM
environment variable. It accepts either a name or a path. Setting this to the empty string will disable pinentry
and fall back to the CLI interface.AsyncWrite::poll_write
implementation for age::stream::StreamWriter
never returns 0 if there is data to write. This makes StreamWriter
with futures::io::copy
.-R/--recipients-file
flags are provided, and they all point to files that contain only "#" prefixed comments and empty lines.age::armor::ArmoredReadError
, used to wrap armor-specific read errors inside std::io::Error
.age::ssh
:
impl Clone for Identity
age::Encryptor::with_recipients
now returns Option<Encryptor>
, with None
returned if the provided list of recipients is empty (to prevent files being encrypted to no recipients). The recipients
argument is also now Vec<Box<dyn age::Recipient + Send>>
.age::encrypted::Identity::recipients
now returns Vec<Box<dyn age::Recipient + Send>>
.age::Decryptor
now rejects invalid or non-canonical scrypt
recipient stanzas (instead of ignoring or accepting them respectively), matching the age specification.age::armor::ArmoredReader
:
age-core 0.9
.aead 0.5
.age 0.8.1
.age::Decryptor
did not previously require "contributory" behaviour for X25519
recipient stanzas. If an age file has an X25519
recipient stanza with an ephemeral share that is a small-order point, the file could previously be decrypted by any native age identity. To ensure we match the behaviour in the age specification, these files are now rejected as invalid.age::Decryptor
now rejects invalid or non-canonical X25519
recipient stanzas (instead of ignoring or accepting them respectively), matching the age specification.rage
now buffers the output until the input is finished, so the output doesn't get in the way of typing.rage
detects that the file being encrypted starts with the age magic string or armor begin marker (indicating that an age-encrypted file is being double-encrypted). The file is still encrypted.-i/--identity
is present. Previously this could result in scripts hanging forever (given that passphrase decryption is intentionally not scriptable).age::Callbacks::confirm
to request that the user provides confirmation for some action.age::cli_common::file_io::InputReader::is_terminal
age::ssh::ParseRecipientKeyError
, which was previously in the public API but unnameable and could not be matched upon.age::Callbacks
now requires Clone + Send + Sync + 'static
bounds.age::cli_common::file_io::OutputWriter::new
now takes an input_is_tty
boolean argument. If input_is_tty
is set to true
, then if OutputWriter
will write to a stdout TTY, it buffers the entire output so it doesn't get in the way of typing the input, and then writes the buffered output to stdout during OutputWriter::flush
.age_plugin::Callbacks::confirm
age_core::io::{DebugReader, DebugWriter}
age_core::plugin::Error::Unsupported
age_core::plugin::Reply::ok_with_metadata
age_core::plugin
:
Connection::open
now returns the debugging-friendly concrete type Connection<DebugReader<ChildStdout>, DebugWriter<ChildStdin>>
.BidirSend::{send, send_stanza}
now return Ok(Error::Unsupported)
when an unsupported
response is received, instead of Err(io::Error)
, making it easier for plugins to implement fallback strategies.rage
to crash instead of being rejected.age-core
to 0.7.1 to fix a bug where non-canonical recipient stanza
bodies in an age file header would cause a panic instead of being rejected.age-core
to 0.7.1 to fix a bug where non-canonical recipient stanza
bodies in an age file header would cause a panic instead of being rejected.AgeStanza::body
method, with the
stanza parser only checking for valid Base64 characters. This caused the
parser to start accepting stanzas with non-canonical last body lines (where
the Base64 encoding would have trailing bits that could not be decoded into
full bytes); calling AgeStanza::body
on these stanzas would cause a panic.
This release fixes the parser to reject non-canonical last body lines, turning
the panic back into an error.-i/--identity
now accepts passphrase-encrypted age identity files.-j PLUGIN_NAME
flag, which allows decrypting with a plugin using its "default mode" (in which no identity-specific information is required). This flag is equivalent to using -i/--identity
with an identity file containing the default plugin identity (containing no data).*-linux.tar.gz
release binaries are now built with Ubuntu 18.04, and require a system with a minimum of glibc 2.27
.age::encrypted::Identity
, for decrypting files with passphrase-encrypted age identity files.age::IdentityFileEntry
enum, representing the possible kinds of entries within an age identity file.age::{DecryptError, EncryptError, PluginError}: Clone
bounds.age::cli_common::UiCallbacks: Clone + Copy
bounds.age::cli_common::Passphrase::random
, for generating a secure passphrase.age::cli_common::ReadError
age::secrecy
, which re-exports the secrecy
crate.age::IdentityFile::into_identities
now returns Vec<IdentityFileEntry>
.age::cli_common::read_identities
:
max_work_factor
parameter for controlling the work factor when decrypting encrypted identities.filenames
(and top-to-bottom from within each file). Plugin identities are no longer coalesced; there is one Box<dyn Identity>
per plugin identity.age::cli_common::ReadError
is now returned instead of a user-specified error type. The error constructor parameters have been removed from the function.age::Callbacks::prompt
has been renamed to Callbacks::display_message
.
age::cli_common::UiCallbacks::display_message
no longer uses pinentry
(which displays a temporary prompt that can be dismissed), so the message is now part of the visible CLI output.IdentityFile::split_into
(replaced by IdentityFileEntry::Plugin
).age_plugin::Callbacks
methods now return age_core::plugin::Error
instead of ()
for internal errors, following changes to age_core::plugin::Result
.age_core::secrecy
, which re-exports the secrecy
crate.age_core::plugin::Error
body
property of age_core::format::AgeStanza
has been replaced by the AgeStanza::body
method, to enable enclosing parsers to defer Base64 decoding until the very end.age_core::plugin::Result
now only takes a single generic argument, and uses age_core::plugin::Error
for its inner error type.age-plugin
crate provides a Rust API for building age plugins.-R/--recipients-file
flag, which accepts a path to a file containing age recipients, one per line (ignoring "#" prefixed comments and empty lines).-e/--encrypt
flag, to allow encryption to be an explicit choice (instead of relying on -d/--decrypt
not being present).-o/--output
will now overwrite existing files instead of returning an error. This makes the behaviour consistent with most UNIX tools, as well as when using pipes.rage
might not decrypt with previous beta versions, due to changes in how stanza bodies are canonically encoded. This should only affect a small fraction of files (if grease that triggers the change is added, which has a 3% chance per file).-r/--recipient
now has the specific type "recipient" which better reflects its name, rather than the ambiguous "source of recipients" it was previously.-i/--identity
can now be used when encrypting files. This requires the -e/--encrypt
flag (to prevent ambiguity, e.g. if the user wants to decrypt but forgets the -d/--decrypt
flag).-r/--recipient
(use -R/--recipients-file
instead).rage
, and there are many decisions that need to be made when downloading a file (e.g. what roots to trust?) that go beyond the APIs we want to focus on here. Users should use a tool like curl
or wget
to download a recipients file, and then pass it to rage
.LANG
variable) being printed to stderr while the program succeeds (which is confusing for users). The previous behaviour can be configured by setting the environment variable RUST_LOG=error
.StreamReader::seek(SeekFrom::End(offset))
did not previously authenticate the ciphertext length; if the ciphertext had been truncated or extended by adversary_offset
, it would instead seek to offset + adversary_offset
. This allowed an adversary with temporary control of an encrypted age file to control the location of a plaintext read following a seek-from-end. age
now returns an error if the last chunk is invalid.
rage
was not affected by this security issue, as it does not use Seek
.rage-mount
may have been affected; it does not use SeekFrom::End
directly, but the tar
or zip
crates might do so.plugin
feature flag:
age::plugin::{Identity, Recipient}
structs for parsing plugin recipients and identities from strings.age::plugin::RecipientPluginV1
, which implements age::Recipient
and runs the V1 recipient plugin protocol.age::plugin::IdentityPluginV1
, which implements age::Identity
and runs the V1 identity plugin protocol.web-sys
feature flag, which enables calculating the work factor for passphrase encryption with the Performance timer via the web-sys
crate, when compiling for a WebAssembly target such as wasm32-unknown-unknown
. This feature is ignored for the wasm32-wasi
target, which supports std::time::SystemTime
.age::Callbacks::request_public_string
to request non-private input from the user (which will not trigger any OS-level passphrase-style prompt, unlike Callbacks::request_passphrase
).age::cli_common::file_io::OutputWriter::File
will now overwrite the file if it exists, instead of returning an error. This makes it consistent with age::cli_common::file_io::OutputWriter::Stdout
, as well as most UNIX tools.age
might not decrypt with previous beta versions, due to changes in how stanza bodies are canonically encoded. This should only affect a small fraction of files (if grease that triggers the change is added, which has a 3% chance per file).age::decryptor::RecipientsDecryptor
now takes impl Iterator<Item = &'a dyn Identity>
in its decryption methods, to make decrypting multiple files with the same identities easier.age::cli_common::file_io::OutputWriter::File
now wraps a LazyFile
struct (instead of wrapping std::io::File
directly), which does not open the file until it is first written to.age::decryptor::Callbacks
has been moved to age::Callbacks
, as it is no longer decryption-specific.age::cli_common::read_identities
now allows either kind of line ending in SSH identity files.en-US
language strings are now always loaded, even if translations are not loaded by calling age::localizer().select(&requested_languages)
.StreamReader::seek(SeekFrom::End(0))
now seeks to the correct position when the plaintext is an exact multiple of the chunk size.Initial beta release!
age_core::primitives::aead_decrypt
now takes a size
argument, checked against the plaintext length. This is to mitigate multi-key attacks, where a ciphertext can be crafted that decrypts successfully under multiple keys. Short ciphertexts can only target two keys, which has limited impact. See this commit message for more details.age_core::format::FILE_KEY_BYTES
constant.age_core::plugin
module, which contains common backend logic used by both the age
library (to implement client support for plugins) and the age-plugin
library.->
and trailing newline are now formal parts of the age stanza; age_core::format::write::age_stanza
now includes them in its output, and age_core::format::read::age_stanza
expects them to be present.age_core::format::write::age_stanza
outputs the new encoding, and age_core::format::read::age_stanza
accepts only the new encoding. The new API age_core::format::read::legacy_age_stanza
accepts either kind of stanza body encoding (the legacy minimal encoding, and the new explicit encoding).i18n-embed-fl 0.3
and i18n-embed 0.10.2
to fix a
transient dependency breakage, that broke cargo install rage
because
cargo install
ignores Cargo.lock
.