# Changelog
All notable changes to the age crate will be documented in this file. Changes
to the [age-core crate](../age-core/CHANGELOG.md) also apply to the age crate,
and are not duplicated here.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to Rust's notion of
[Semantic Versioning](https://semver.org/spec/v2.0.0.html). All versions prior
to 1.0.0 are beta releases.

## [Unreleased]

## [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
  arbitrary binary under certain conditions. See GHSA-4fg7-vxc8-qx5w. Plugin
  names are now required to only contain alphanumeric characters or the four
  special characters `+-._`. Thanks to ⬡-49016 for reporting this issue.

## [0.11.0] - 2024-11-03
### Added
- New streamlined APIs for use with a single recipient or identity and a small
  amount of data (that can fit entirely in memory):
  - `age::encrypt`
  - `age::encrypt_and_armor`
  - `age::decrypt`
- `age::Decryptor::{decrypt, decrypt_async, is_scrypt}`
- `age::IdentityFile::to_recipients`
- `age::IdentityFile::with_callbacks`
- `age::IdentityFile::write_recipients_file`
- `age::IdentityFileConvertError`
- `age::NoCallbacks`
- `age::scrypt`, providing recipient and identity types for passphrase-based
  encryption.
- Partial French translation!

### Changed
- Migrated to `i18n-embed 0.15`, `secrecy 0.10`.
- `age::Encryptor::with_recipients` now takes recipients by reference instead of
  by value. This aligns it with `age::Decryptor` (which takes identities by
  reference), and also means that errors with recipients are reported earlier.
  This causes the following changes to the API:
  - `Encryptor::with_recipients` takes `impl Iterator<Item = &'a dyn Recipient>`
    instead of `Vec<Box<dyn Recipient + Send>>`.
  - Verification of recipients and generation of stanzas now happens in
    `Encryptor::with_recipients` instead of `Encryptor::wrap_output` and
    `Encryptor::wrap_async_output`.
  - `Encryptor::with_recipients` returns `Result<Self, EncryptError>` instead of
    `Option<Self>`, and `Encryptor::{wrap_output, wrap_async_output}` return
    `io::Result<StreamWriter<W>>` instead of `Result<StreamWriter<W>, EncryptError>`.
  - `age::EncryptError` has a new variant `MissingRecipients`, taking the place
    of the `None` that `Encryptor::with_recipients` could previously return.
- `age::Decryptor` is now an opaque struct instead of an enum with `Recipients`
  and `Passphrase` variants.
- `age::IdentityFile` now has a `C: Callbacks` generic parameter, which defaults
  to `NoCallbacks`.
- `age::IdentityFile::into_identities` now returns
  `Result<Vec<Box<dyn crate::Identity>>, DecryptError>` instead of
  `Vec<IdentityFileEntry>`.
- `age::Recipient::wrap_file_key` now returns `(Vec<Stanza>, HashSet<String>)`:
  a tuple of the stanzas to be placed in an age file header, and labels that
  constrain how the stanzas may be combined with those from other recipients.
- `age::plugin::RecipientPluginV1` now supports the labels extension.

### Fixed
- `age::cli_common::read_identities` once again correctly parses identity files
  that are a single line without a trailing newline. This broke in 0.10.0 due to
  an unrelated refactor.

### Removed
- `age::decryptor::PassphraseDecryptor` (use `age::Decryptor` with
  `age::scrypt::Identity` instead).
- `age::decryptor::RecipientsDecryptor` (use `age::Decryptor` instead).
- `age::IdentityFileEntry`

## [0.10.0] - 2024-02-04
### Added
- Russian translation!
- `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`

### Changed
- MSRV is now 1.65.0.
- Migrated to `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`
  - The following trait implementations now return
    `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`

### Fixed
- `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.
- Weak `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.

## [0.9.2] - 2023-06-12
### Added
- `age::Decryptor::{new_buffered, new_async_buffered}`, which are more efficient
  for types implementing `std::io::BufRead` or `futures::io::AsyncBufRead`
  (which includes `&[u8]` slices).
- `impl std::io::BufRead for age::armor::ArmoredReader`
- `impl futures::io::AsyncBufRead for age::armor::ArmoredReader`

### Changed
- The `pinentry` binary used by `age::cli_common::read_secret` can now be set
  manually with the `PINENTRY_PROGRAM` environment variable. It accepts either a
  binary name or a path. Setting this to the empty string will disable `pinentry`
  usage and fall back to the CLI interface.

### Fixed
- The `AsyncWrite::poll_write` implementation for `age::stream::StreamWriter`
  now never returns 0 if there is data to write. This makes `StreamWriter`
  compatible with `futures::io::copy`.

## [0.9.1] - 2023-03-24
### Added
- Support for encrypted OpenSSH keys exported from 1Password.

## [0.9.0] - 2022-10-27
### Security
- `age::ssh::Recipient::SshRsa` now has a maximum modulus size of 4096 bits, to
  prevent a Denial of Service (DoS) condition when encrypting to untrusted
  public keys.

### Added
- `age::armor::ArmoredReadError`, used to wrap armor-specific read errors inside
  `std::io::Error`.
- `age::ssh`:
  - `impl Clone for Identity`

### Changed
- MSRV is now 1.59.0.
- Migrated to `rsa 0.7`.
- `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>>`.

### Fixed
- `age::Decryptor` now rejects invalid or non-canonical `scrypt` recipient
  stanzas (instead of ignoring or accepting them respectively), matching the
  [age specification](https://c2sp.org/age#scrypt-recipient-stanza).
- `age::armor::ArmoredReader`:
  - It now correctly implements strict parsing as defined in
    [RFC 7468](https://www.rfc-editor.org/rfc/rfc7468.html#section-3), and
    rejects armored files with non-canonical final lines (where padding bytes
    are omitted).
  - It now rejects armored files with non-whitespace characters after the end
    marker.
  - It now accepts armored files with no newline after the end marker.
    Previously these were rejected by the synchronous API, and would cause the
    async API to hang.
  - The async API now correctly rejects some classes of invalid armoring that
    previously would cause it to hang.

## [0.8.1] - 2022-06-18
### Security
- `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](https://c2sp.org/age#x25519-recipient-stanza), these
  files are now rejected as invalid.

### Fixed
- `age::Decryptor` now rejects invalid or non-canonical `X25519` recipient
  stanzas (instead of ignoring or accepting them respectively), matching the
  [age specification](https://c2sp.org/age#x25519-recipient-stanza).

## [0.8.0] - 2022-05-02
### Added
- `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.

### Changed
- MSRV is now 1.56.0.
- `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`.
- Ciphertexts are now required to end in a non-empty STREAM chunk, unless it is
  the only chunk (meaning that the plaintext is empty). Neither age nor rage
  generate non-empty files ending in an empty chunk, instead marking the final
  full chunk as the last chunk.

## [0.7.1] - 2021-12-27
### Fixed
- Bumped `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.

## [0.7.0] - 2021-10-18
### Added
- `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.

### Changed
- MSRV is now 1.51.0.
- `age::IdentityFile::into_identities` now returns `Vec<IdentityFileEntry>`.
- `age::cli_common::read_identities`:
  - Encrypted age files will now be parsed and assumed to be encrypted age
    identities. This assumption is checked at file-decryption time.
  - New `max_work_factor` parameter for controlling the work factor when
    decrypting encrypted identities.
  - Identities are now returned in the same order as `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.

### Removed
- `IdentityFile::split_into` (replaced by `IdentityFileEntry::Plugin`).

## [0.6.0] - 2021-05-02
### Security
- `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.

### Added
- Plugin support, enabled by the `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.
- The `web-sys` feature flag, which enables calculating the work factor for
  passphrase encryption with the
  [Performance timer](https://developer.mozilla.org/en-US/docs/Web/API/Performance)
  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`](https://doc.rust-lang.org/stable/std/time/struct.SystemTime.html#underlying-system-calls).
- `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`).

### Changed
- MSRV is now 1.47.0.
- `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.
- Files encrypted with this version of `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.

### Fixed
- `age::cli_common::read_identities` now allows either kind of line ending in
  SSH identity files.
- Default `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.

## [0.5.1] - 2021-02-13
### Fixed
- Bumped dependencies to `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`](https://github.com/rust-lang/cargo/issues/7169).

## [0.5.0] - 2020-11-22
### Added
- Italian, Spanish, and Chinese translations!
- New core traits, implemented by all relevant `age` types:
  - `age::Identity`, representing an identity that can decrypt an age file.
  - `age::Recipient`, representing a potential recipient of an age file.
- Separate modules and structs for different recipient types:
  - `age::x25519`
  - `age::ssh` (behind the `ssh` feature flag).
- `age::EncryptError`, representing errors that can occur during encryption.
- `age::IdentityFile` struct, for parsing a list of native age identities
  (currently only `age::x25519::Identity`) from a file.
- Asynchronous APIs for encryption and decryption, enabled by the `async`
  feature flag:
  - `age::Encryptor::wrap_async_output()`
  - `age::Decryptor::new_async()`
  - `age::decryptor::RecipientsDecryptor::decrypt_async()`
  - `age::decryptor::PassphraseDecryptor::decrypt_async()`
- Explicit armoring support, enabled by the `armor` feature flag:
  - `age::armor::ArmoredReader`, which can be wrapped around an input to handle
    a potentially-armored age file.
  - `age::armor::ArmoredWriter`, which can be wrapped around an output to
    optionally apply the armored age format.

### Changed
- MSRV is now 1.45.0.
- Changes due to the new core traits:
  - `age::Encryptor::with_recipients` now takes `Vec<Box<dyn Recipient>>`.
  - `age::decryptor::RecipientsDecryptor` now takes
    `impl Iterator<Item = Box<dyn Identity>>` in its decryption methods.
  - `age::cli_common::read_identities` now returns `Vec<Box<dyn Identity>>`, as
    it abstracts over `age::IdentityFile` and `age::ssh::Identity`. When the
    `ssh` feature flag is enabled, it also takes an `unsupported_ssh` argument
    for handling unsupported SSH identities.
  - `age::Error` has been renamed to `age::DecryptError`.
- Changes due to explicit armoring support:
  - `age::Encryptor::wrap_output` now only generates the non-malleable binary
    age format. To optionally generate armored age files, use
    `encryptor.wrap_output(ArmoredWriter::wrap_output(output, format))`.
  - `age::Decryptor` now only decrypts the non-malleable binary age format. To
    handle age files that are potentially armored, use
    `Decryptor::new(ArmoredReader::new(input))`.
  - `age::Format` has been moved to `age::armor::Format`.
- SSH support is now disabled by default, behind the `ssh` feature flag.
  `ssh-rsa` keys are now supported without the `unstable` feature flag.
- `age::Callbacks` has been moved to `age::decryptor::Callbacks`.

### Removed
- `age::SecretKey` (replaced by `age::x25519::Identity` and
  `age::ssh::Identity`).
- `age::keys::RecipientKey` (replaced by `age::x25519::Recipient` and
  `age::ssh::Recipient`).
- `age::keys::{Identity, IdentityKey}` (replaced by `age::Identity` trait on
  individual identities, and `age::IdentityFile` for parsing identities).
- `age::decryptor::RecipientsDecryptor::decrypt_with_callbacks()` (identities
  are now expected to handle their own callbacks, and
  `age::cli_common::read_identities` now adds callbacks to SSH identities).
- Default identity path:
  - `age::cli_common::get_config_dir`.
  - The `no_default` parameter for `age::cli_common::read_identities`.

## [0.4.0] - 2020-03-25
### Added
- `age::Decryptor::new(R: Read)`, which parses an age file header and returns
  a context-specific decryptor.
- `age::decryptor` module containing the context-specific decryptors.
  - Their decryption methods return the concrete type `StreamReader<R>`,
    enabling them to handle seekable readers.
- `age::Encryptor::with_recipients(Vec<RecipientKey>)`
- `age::Encryptor::with_user_passphrase(SecretString)`
- Support for encrypted OpenSSH keys created with `ssh-keygen` prior to OpenSSH
  7.6.
- `age::cli_common::file_io::OutputWriter::is_terminal`

### Changed
- `age::Decryptor` has been refactored to auto-detect the decryption type. As a
  result, both identity-based and passphrase-based decryption need to be
  handled.
- `age::StreamReader` has been moved into the `age::stream` module, along with
  `StreamWriter` which was previously public but has now been formally exposed
  in the API for documentation purposes.
- `age::Encryptor` is now an opaque struct, and must be created via its new
  constructors.
- `age::Encryptor::wrap_output` now consumes `self`, making it harder to
  accidentally reuse a passphrase for multiple encrypted files.
- `age::cli_common::read_identities` now takes an additional `file_not_found`
  parameter for customising the error when an identity filename is not found.

### Removed
- `age::Decryptor::trial_decrypt` (replaced by context-specific decryptors).
- `age::Decryptor::trial_decrypt_seekable` (merged into the context-specific
  decryptors).
- `age::Error::ArmoredWhenSeeking`
- `age::Error::MessageRequiresKeys`
- `age::Error::MessageRequiresPassphrase`

### Fixed
- Key files with Windows line endings are now correctly parsed.

## [0.3.1] - 2020-02-11
### Fixed
- Bumped dependencies to `cookie-factory ^0.3.1` to fix nightly builds.

## [0.3.0] - 2020-02-09
### Added
- `age::Callbacks`, which encapsulates any requests that might be necessary
  during the decryption process.
- `age::cli_common::UiCallbacks`, which implements `Callbacks` with requests to
  the user via `age::cli_common::read_secret`.
- `age::Decryptor::with_identities(Vec<Identity>)`
- `age::Decryptor::with_identities_and_callbacks(Vec<Identity>, Box<dyn Callbacks>)`
- `age::Encryptor` will insert a random recipient stanza into the header, to
  keep age's joint well oiled.

### Changed
- The CLI tools have been moved into the `rage` crate.
- The `age::Decryptor::Keys` enum case has been renamed to `Identities` and
  altered to store a `Box<dyn Callbacks>` internally.
- `age::Decryptor::trial_decrypt` and `age::Decryptor::trial_decrypt_seekable`
  both no longer take a `request_passphrase` argument.
- `age::cli_common::read_secret`:
  - Takes an additional `prompt` parameter.
  - Uses the system `pinentry` binary for requesting secrets if available.
  - Returns `pinentry::Error` instead of `io::Error`.
- `age::cli_common::read_or_generate_passphrase` now returns `pinentry::Error`
  instead of `io::Error`.
- Core age parsers and serializers have been moved into the `age-core` crate.

### Fixed
- Fixed several crashes in the armored format reader, found by fuzzing. The
  reader also now correctly enforces a canonical armor marker and line lengths.
- Recipient stanzas with empty bodies are correctly parsed.

## [0.2.0] - 2020-01-10
### Added
- The library crate can be compiled to WASM.
- When encrypting to a passphrase, rage will generate a secure passphrase if the
  user does not provide one.
- `SecretKey::to_string -> secrecy::SecretString`, which zeroizes most internal
  state. (Zeroizing all internal state requires changes to the `bech32` crate.)
- `RecipientKey` implements `Display`, and can be converted to a string using
  `recipient.to_string()`.
- `Decryptor::with_passphrase` constructor.
- `--max-work-factor WF` argument for rage and rage-mount, to enable overriding
  the default maximum (which is around 16 seconds of work).

### Changed
- `age::Encryptor::wrap_output` now takes an `age::Format` enum argument instead
  of a boolean flag.
- Recipients are now parsed as filenames last instead of first. If a filename
  happens to also be a valid recipient format, the file will be ignored. This
  can be overridden by using an absolute file path.
- The filename `-` (hyphen) is now treated as an explicit request to read from
  standard input or write to standard output when used as an input or output
  filename.
- `-o -` will override protections for terminals when standard output is not
  being piped elsewhere: output will not be truncated, and binary data will be
  printed directly to the terminal.
- Armored encrypted output can now be printed to the terminal. Large files will
  be truncated (to protect the terminal), corrupting the encryption. This can be
  overriden with `-o -`.
- The `Decryptor::Passphrase` enum case has been altered to store an optional
  maximum work factor.

### Removed
- `SecretKey::to_str` (replaced by `SecretKey::to_string`).
- `RecipientKey::to_str` (replaced by `Display` implementation and
  `recipient.to_string()`).

### Fixed
- Corrected encoding of example recipients in manpages.
- Re-enabled the default identities file (#41).
- Fixed parser to reject encrypted OpenSSH keys if they contain invalid
  `bcrypt_pbkdf` parameters.
- [Unix] `rage-keygen -o filename` now creates files with mode `600` (i.e. the
  output file is no longer world-readable).
- Unknown recipient lines are now parsed and ignored during decryption, instead
  of causing a hard failure.

## [0.1.1] - 2019-12-29
### Added
- Debian packaging support via `cargo deb`. See [docs/debian.md](../docs/debian.md)
  for details.

### Changed
- Moved the `num_traits` dependency behind the `unstable` feature flag.
- The `generate-docs` example now generates (the equivalent of ) `gzip -9`
  manpages, for ease of use in Debian packaging.

### Fixed
- Decrypted chunks inside the STREAM implementation are now zeroized after use.

## [0.1.0] - 2019-12-27

Initial beta release!
