Skip to content

Releases: FiloSottile/age

v1.3.1

28 Dec 12:03

Choose a tag to compare

v1.3.1 is a minor release to restore version injection from downstream package build processes.

See the v1.3.0 release notes for an overview of recent additions.

age v1.3.0: post-quantum (and more)!

27 Dec 22:33

Choose a tag to compare

The age logo, a wireframe of St. Peters dome in Rome, with the text: age, file encryption

Exactly six years after the first age beta release, v1.3.0 brings post-quantum resistance to age, along with a couple long-requested features, built-in support for recipients compatible with hardware plugins, I/O API improvements, and many usability enhancements.

Post-quantum recipients

age now has native post-quantum recipients based on HPKE with a hybrid ML-KEM-768 KEM. The recipients start with age1pq1..., and the identities start with AGE-SECRET-KEY-PQ-1....

To generate a post-quantum keypair:

$ age-keygen -pq

If you have your own age implementation, C2SP has the specification, and CCTV has test vectors for the new hybrid recipient types.

(If you are using an older age client, an optional plugin is available that provides out-of-the-box support for encryption to hybrid recipients. Hybrid identities can be converted to work with the plugin with age-plugin-pq -identity.)

New I/O APIs

The new DecryptReaderAt API implements seeking decryption, which can be used with zip.NewReader.

The new EncryptReader API implements pull-based encryption by wrapping an io.Reader, as opposed to wrapping an io.Writer like Encrypt.

age-inspect

The new age-inspect(1) tool presents the metadata of an age file without decrypting it.

hello.age is an age file, version "age-encryption.org/v1".

This file is ASCII-armored.

This file is encrypted to the following recipient types:
  - "mlkem768x25519"

This file uses post-quantum encryption.

Size breakdown (assuming it decrypts successfully):

    Header                      1627 bytes
    Armor overhead              1350 bytes
    Encryption overhead           32 bytes
    Payload                     1959 bytes
                        -------------------
    Total                       4968 bytes

Tip: for machine-readable output, use --json.

Built-in recipients compatible with hardware plugins

age(1) now supports encrypting to age1tag1... and age1tagpq1... recipients, which are compatible with hardware plugins. The latest or upcoming versions of age-plugin-yubikey, age-plugin-tpm, and age-plugin-se will support producing these recipients for existing and new keys, and decrypting files encrypted to them.

The filippo.io/age/tag package provides an implementation, including a Recipient.Tag method useful to plugin implementations.

C2SP has the specification for these recipient types.

(For users that are using older age clients, there are compatiblity plugins that can be shipped along with the hardware plugins.)

Non-interactive passphrase input

We have long resisted implementing non-interactive CLI passphrase input, because most use cases are more securely and/or efficiently served by native keys or passphrase-encrypted identities. However, there are some residual use cases.

This is now available through the batchpass plugin, but we invite everyone to read the warning in the man page and help text.

Go framework for implementing plugins

The filippo.io/age/plugin package now provides a framework for exposing age.Recipient and age.Identity implementations as standalone CLI plugins.

For example, this is the entire age-plugin-tagpq compatibility implementation:

package main

import (
	"log"
	"os"

	"filippo.io/age"
	"filippo.io/age/plugin"
	"filippo.io/age/tag"
)

func main() {
	p, err := plugin.New("tagpq")
	if err != nil {
		log.Fatal(err)
	}
	p.HandleRecipient(func(b []byte) (age.Recipient, error) {
		return tag.NewHybridRecipient(b)
	})
	os.Exit(p.Main())
}

Moreover, the new plugin.NewTerminalUI function provides a ready-to-use terminal-based ClientUI implementation for the existing plugin client API.

Armor fixes

The CLI now allows some leading whitespace before the armor header, as the API always did. The API and CLI now reject empty lines in armored data, as required by the spec.

Detached headers

The new set of APIs ExtractHeader, DecryptHeader, and NewInjectedFileKeyIdentity allow working with detached age headers, where the file key is unwrapped separately from the actual file decryption. This can be useful for efficiency or to delegate unwrapping without exposing the file contents.

Usability improvements

The CLI will now refuse to output decrypted binary data to the terminal. (We did this before attending the gpg.fail talk!)

Native identities are now tried first when decrypting through both the CLI and the API, since they can't require interaction. Order is otherwise preserved.

NoIdentityMatchError now exposes the stanza types of the header, unwraps to the errors returned by the Identity.Unwrap method calls, and prints the underlying error if only one identity was provided.

Virtual terminal processing is now enabled on Windows terminals that don't enable it by default, so that ANSI escape sequences work as expected.

Many improved and more proactively helpful error messages.

The release artifacts now include a source tarball with vendored dependencies.

age v1.2.1: security fix

18 Dec 15:26

Choose a tag to compare

This release fixes a security vulnerability that could allow an attacker to execute an arbitrary binary under certain conditions.

See GHSA-32gq-x56h-299c.

Plugin names may now only contain alphanumeric characters or the four special characters +-._.

Thanks to ⬡-49016 for reporting this issue.

age v1.2.0

16 Jun 14:11

Choose a tag to compare

A small release to build the release binaries with a more recent Go toolchain, and to fix a couple CLI edge cases (#491, #555).

The Go module now exposes a plugin package that provides an age plugin client. That is, Recipient and Identity implementations that invoke a plugin binary, allowing the use of age plugins in Go programs.

Finally, Recipients can now return a set of "labels" by implementing RecipientWithLabels. This allows replicating the special behavior of the scrypt Recipient in third-party Recipients, or applying policy useful for authenticated or post-quantum Recipients.

// RecipientWithLabels can be optionally implemented by a Recipient, in which
// case Encrypt will use WrapWithLabels instead of Wrap.
//
// Encrypt will succeed only if the labels returned by all the recipients
// (assuming the empty set for those that don't implement RecipientWithLabels)
// are the same.
//
// This can be used to ensure a recipient is only used with other recipients
// with equivalent properties (for example by setting a "postquantum" label) or
// to ensure a recipient is always used alone (by returning a random label, for
// example to preserve its authentication properties).
type RecipientWithLabels interface {
	WrapWithLabels(fileKey []byte) (s []*Stanza, labels []string, err error)
}

age v1.1.1

26 Dec 20:39

Choose a tag to compare

age v1.1.1 is a patch release to fix go install filippo.io/age/...@latest.

See the release notes for v1.1.0 for changes since v1.0.0.

age v1.1.0: plugin and YubiKeys support

26 Dec 14:57

Choose a tag to compare

The age logo, a wireframe of St. Peters dome in Rome, with the text: age, file encryption

age is a simple, modern and secure file encryption tool, format, and Go library. It features small explicit keys, no config options, and UNIX-style composability. Learn more by reading the README, the age(1) man page, the Go API reference, the format specification, or the full release changelog. Watch the repository or follow @filippo@abyssdomain.expert to be notified of new releases.

🛠️ FYI, age now has an extensive test suite which all age implementations are encouraged to adopt.

Plugin support

The age CLI now supports plugins, such as age-plugin-yubikey by @str4d. To try it on macOS with Homebrew:

$ brew upgrade age
$ brew install age-plugin-yubikey
$ age-plugin-yubikey # interactive setup
$ age -r age1yubikey1qwt50d05nh5vutpdzmlg5wn80xq5negm4uj9ghv0snvdd3yysf5yw3rhl3t
$ age -d -i age-yubikey-identity-388178f3.txt

Plugins must be loaded explicitly by using their respective recipient or identity, and are not tied to a specific header stanza type. This means plugins can be used not only to support new recipient types such as PIV tokens (i.e. YubiKeys) or cloud KMS solutions, but also to produce passphrase-encrypted files that can be decrypted without plugins, to store age native private keys on secure elements, or even for agent functionality or to proxy decryption operations to remote machines.

Plugins operate over a simple textual stdin/stdout protocol (C2SP/C2SP#5). Developers are encouraged to reach out with plugin ideas and announcements. Read more in the relevant man page section.

Breaking changes

If -i is used, passphrase-encrypted files are now rejected. Previously, a passphrase-encrypted file was auto-detected and the identity file was ignored. This could lead to unexpected behavior, such as a script blocking for user interaction, based on potentially untrusted input files. Now, age -d must be invoked without -i arguments to decrypt passphrase-encrypted files. A helpful error is printed otherwise. This should not break any automated system as passphrase decryption was always interactive.

Empty final chunks are now rejected. If a payload was a multiple of 64KiB long, there were two valid encryptions for it: with a "full" last chunk encrypting 64KiB, or with an additional "empty" chunk encrypting 0 bytes. age, rage, and all other known implementations only ever produced the former. (Note that age will forever decrypt files it generated.) The latter is now rejected. The specification has been updated (C2SP/C2SP#13) and test cases are included in the test suite.

Minor changes

PKCS#8-encoded Ed25519 private keys (such as 1Password exports) are now supported as SSH identities.

If an armored file is pasted into the terminal, age will now attempt to wait until the end of the file before prompting for a password.

Some invalid files are now correctly rejected, in particular encrypted files with trailing data. (Yay for the test suite!)

If /dev/tty is present but can't be opened, age will now fallback to trying to treat stdin as a terminal as if /dev/tty wasn't present. (Thanks @brandsimon!)

Input prompts now go to the terminal, even if standard error is redirected.

Values of the new armor.Error type are now returned wrapped in decryption errors when appropriate.

Windows binary releases are now signed. (Thanks @technion!)

Documentation and error messages were improved.

age v1.1.0-rc.1: plugin and YubiKeys support

11 Jun 16:00

Choose a tag to compare

The age logo, an wireframe of St. Peters dome in Rome, with the text: age, file encryption

age is a simple, modern and secure file encryption tool, format, and Go library. It features small explicit keys, no config options, and UNIX-style composability. Learn more by reading the README, the age(1) man page, the Go API reference, the format specification, or the full release changelog. Watch the repository or follow @FiloSottile on Twitter to be notified of new releases.

v1.1.0-rc.1 is the first release candidate of v1.1.0. Users are encourage to test the new release and especially the new features listed below. Issue or UX reports in advance of the final release are greatly appreciated.

📃 In case you missed it: a new, more polished version of the age format specification has been published.

Plugin support

The age CLI now supports plugins, such as age-plugin-yubikey by @str4d. To test it on macOS with Homebrew:

$ brew install --HEAD age
$ brew install age-plugin-yubikey
$ age-plugin-yubikey # interactive setup
$ age -r age1yubikey1qwt50d05nh5vutpdzmlg5wn80xq5negm4uj9ghv0snvdd3yysf5yw3rhl3t
$ age -d -i age-yubikey-identity-388178f3.txt

Plugins must be loaded explicitly by using their respective recipient or identity, and are not tied to a specific header stanza type. This means plugins can be used not only to support new recipient types such as PIV tokens (i.e. YubiKeys) or cloud KMS solutions, but also to produce passphrase-encrypted files that can be decrypted without plugins, to store age native private keys on secure elements, or even for agent functionality or to proxy decryption operations to remote machines.

Plugins operate over a simple textual stdin/stdout protocol (C2SP/C2SP#5). Developers are encouraged to reach out with plugin ideas and announcements. Read more in the relevant man page section.

CLI breaking changes

If -i is used, passphrase-encrypted files are now rejected. Previously, a passphrase-encrypted file was auto-detected and the identity file was ignored. This could lead to unexpected behavior, such as a script blocking for user interaction, based on potentially untrusted input files. Now, age -d must be invoked without -i arguments to decrypt passphrase-encrypted files. A helpful error is printed otherwise. This should not break any automated system as passphrase decryption was always interactive.

Empty final chunks are now rejected. If a payload was a multiple of 64KiB long, there were two valid encryptions for it: with a "full" last chunk encrypting 64KiB, or with an additional "empty" chunk encrypting 0 bytes. age, rage, and all other known implementations only ever produced the former. (Note that age will forever decrypt files it generated.) The latter is now rejected. The specification is being updated (C2SP/C2SP#13) and test cases will be provided.

Minor changes

If /dev/tty is present but can't be opened, age will now fallback to trying to treat stdin as a terminal as if /dev/tty wasn't present.

Windows binary releases are now signed.

Documentation and error messages were improved.

age v1.0.0 🏁

06 Sep 16:48
552aa0a

Choose a tag to compare

The age logo, an wireframe of St. Peters dome in Rome, with the text: age, file encryption

age—pronounced [aɡe̞], like the Italian “aghe”—is a simple, modern and secure file encryption tool, format, and Go library.

It features small explicit keys, no config options, and UNIX-style composability.

$ age-keygen -o key.txt
Public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
$ tar cvz ~/data | age -r age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p > data.tar.gz.age
$ age --decrypt -i key.txt data.tar.gz.age > data.tar.gz

v1.0.0 is the first stable release of the Go CLI and API, twenty months after the first beta.

Learn more by reading the README, the age(1) man page, the Go API reference, or the original design document.

Watch the repository or follow @FiloSottile on Twitter to be notified of new releases.

Never two without three

15 Jun 12:19

Choose a tag to compare

Pre-release

Maybe™️ actually™️ the last v1.0.0 release candidate!

Encrypted identity files are now supported. Regular passphrase-encrypted age files can be used with -i, the passphrase will be requested interactively, and the decrypted file will be read line-by-line as a standard identity file.

Passphrases can now be requested interactively from the terminal on Windows even if standard input is in use.

Errors are now tidier and all start with age: error: .

The last (?) v1.0.0 release candidate!

26 May 11:49

Choose a tag to compare

Pre-release

This is it! The v1.0.0 milestone is empty. Will let it simmer for a bit and then we'll have v1.0.0. Please test it!

Main changes

🤏 Reject RSA keys (for ssh-rsa) smaller than 2048 bits (#266)

🥞 Remove limit of 20 recipients per file (#139)

📜 Add age(1) and age-keygen(1) man pages (#131) — read them here!

⚔️ Fix armored encoding for files of certain lengths (#263)

Minor changes

  • Add freebsd/amd64 and darwin/arm64 builds
  • Use filippo.io/edwards25519 for Ed25519 to Curve25519 conversion (again)
  • Check Close() error on output files (#81)
  • Allow reading both passphrase and input from a terminal (#196)
  • Don't warn about world-readable output for public keys in age-keygen (#267)