age

package module
v1.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 16, 2024 License: BSD-3-Clause Imports: 19 Imported by: 270

README

Go Reference man page C2SP specification

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.

$ 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

📜 The format specification is at age-encryption.org/v1. age was designed by @Benjojo12 and @FiloSottile.

📬 Follow the maintenance of this project by subscribing to Maintainer Dispatches!

🦀 An alternative interoperable Rust implementation is available at github.com/str4d/rage.

🔑 Hardware PIV tokens such as YubiKeys are supported through the age-plugin-yubikey plugin.

✨ For more plugins, implementations, tools, and integrations, check out the awesome age list.

💬 The author pronounces it [aɡe̞] with a hard g, like GIF, and is always spelled lowercase.

Installation

Homebrew (macOS or Linux) brew install age
MacPorts port install age
Alpine Linux v3.15+ apk add age
Arch Linux pacman -S age
Debian 12+ (Bookworm) apt install age
Debian 11 (Bullseye) apt install age/bullseye-backports (enable backports for age v1.0.0+)
Fedora 33+ dnf install age
Gentoo Linux emerge app-crypt/age
NixOS / Nix nix-env -i age
openSUSE Tumbleweed zypper install age
Ubuntu 22.04+ apt install age
Void Linux xbps-install age
FreeBSD pkg install age (security/age)
OpenBSD 6.7+ pkg_add age (security/age)
Chocolatey (Windows) choco install age.portable
Scoop (Windows) scoop bucket add extras && scoop install age
pkgx pkgx install age

On Windows, Linux, macOS, and FreeBSD you can use the pre-built binaries.

https://dl.filippo.io/age/latest?for=linux/amd64
https://dl.filippo.io/age/v1.1.1?for=darwin/arm64
...

If your system has a supported version of Go, you can build from source.

go install filippo.io/age/cmd/...@latest

Help from new packagers is very welcome.

Usage

For the full documentation, read the age(1) man page.

Usage:
    age [--encrypt] (-r RECIPIENT | -R PATH)... [--armor] [-o OUTPUT] [INPUT]
    age [--encrypt] --passphrase [--armor] [-o OUTPUT] [INPUT]
    age --decrypt [-i PATH]... [-o OUTPUT] [INPUT]

Options:
    -e, --encrypt               Encrypt the input to the output. Default if omitted.
    -d, --decrypt               Decrypt the input to the output.
    -o, --output OUTPUT         Write the result to the file at path OUTPUT.
    -a, --armor                 Encrypt to a PEM encoded format.
    -p, --passphrase            Encrypt with a passphrase.
    -r, --recipient RECIPIENT   Encrypt to the specified RECIPIENT. Can be repeated.
    -R, --recipients-file PATH  Encrypt to recipients listed at PATH. Can be repeated.
    -i, --identity PATH         Use the identity file at PATH. Can be repeated.

INPUT defaults to standard input, and OUTPUT defaults to standard output.
If OUTPUT exists, it will be overwritten.

RECIPIENT can be an age public key generated by age-keygen ("age1...")
or an SSH public key ("ssh-ed25519 AAAA...", "ssh-rsa AAAA...").

Recipient files contain one or more recipients, one per line. Empty lines
and lines starting with "#" are ignored as comments. "-" may be used to
read recipients from standard input.

Identity files contain one or more secret keys ("AGE-SECRET-KEY-1..."),
one per line, or an SSH key. Empty lines and lines starting with "#" are
ignored as comments. Passphrase encrypted age files can be used as
identity files. Multiple key files can be provided, and any unused ones
will be ignored. "-" may be used to read identities from standard input.

When --encrypt is specified explicitly, -i can also be used to encrypt to an
identity file symmetrically, instead or in addition to normal recipients.

Multiple recipients

Files can be encrypted to multiple recipients by repeating -r/--recipient. Every recipient will be able to decrypt the file.

$ age -o example.jpg.age -r age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p \
    -r age1lggyhqrw2nlhcxprm67z43rta597azn8gknawjehu9d9dl0jq3yqqvfafg example.jpg
Recipient files

Multiple recipients can also be listed one per line in one or more files passed with the -R/--recipients-file flag.

$ cat recipients.txt
# Alice
age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
# Bob
age1lggyhqrw2nlhcxprm67z43rta597azn8gknawjehu9d9dl0jq3yqqvfafg
$ age -R recipients.txt example.jpg > example.jpg.age

If the argument to -R (or -i) is -, the file is read from standard input.

Passphrases

Files can be encrypted with a passphrase by using -p/--passphrase. By default age will automatically generate a secure passphrase. Passphrase protected files are automatically detected at decrypt time.

$ age -p secrets.txt > secrets.txt.age
Enter passphrase (leave empty to autogenerate a secure one):
Using the autogenerated passphrase "release-response-step-brand-wrap-ankle-pair-unusual-sword-train".
$ age -d secrets.txt.age > secrets.txt
Enter passphrase:

Passphrase-protected key files

If an identity file passed to -i is a passphrase encrypted age file, it will be automatically decrypted.

$ age-keygen | age -p > key.age
Public key: age1yhm4gctwfmrpz87tdslm550wrx6m79y9f2hdzt0lndjnehwj0ukqrjpyx5
Enter passphrase (leave empty to autogenerate a secure one):
Using the autogenerated passphrase "hip-roast-boring-snake-mention-east-wasp-honey-input-actress".
$ age -r age1yhm4gctwfmrpz87tdslm550wrx6m79y9f2hdzt0lndjnehwj0ukqrjpyx5 secrets.txt > secrets.txt.age
$ age -d -i key.age secrets.txt.age > secrets.txt
Enter passphrase for identity file "key.age":

Passphrase-protected identity files are not necessary for most use cases, where access to the encrypted identity file implies access to the whole system. However, they can be useful if the identity file is stored remotely.

SSH keys

As a convenience feature, age also supports encrypting to ssh-rsa and ssh-ed25519 SSH public keys, and decrypting with the respective private key file. (ssh-agent is not supported.)

$ age -R ~/.ssh/id_ed25519.pub example.jpg > example.jpg.age
$ age -d -i ~/.ssh/id_ed25519 example.jpg.age > example.jpg

Note that SSH key support employs more complex cryptography, and embeds a public key tag in the encrypted file, making it possible to track files that are encrypted to a specific public key.

Encrypting to a GitHub user

Combining SSH key support and -R, you can easily encrypt a file to the SSH keys listed on a GitHub profile.

$ curl https://github.com/benjojo.keys | age -R - example.jpg > example.jpg.age

Keep in mind that people might not protect SSH keys long-term, since they are revokable when used only for authentication, and that SSH keys held on YubiKeys can't be used to decrypt files.

Documentation

Overview

Package age implements file encryption according to the age-encryption.org/v1 specification.

For most use cases, use the Encrypt and Decrypt functions with X25519Recipient and X25519Identity. If passphrase encryption is required, use ScryptRecipient and ScryptIdentity. For compatibility with existing SSH keys use the filippo.io/age/agessh package.

age encrypted files are binary and not malleable. For encoding them as text, use the filippo.io/age/armor package.

Key management

age does not have a global keyring. Instead, since age keys are small, textual, and cheap, you are encouraged to generate dedicated keys for each task and application.

Recipient public keys can be passed around as command line flags and in config files, while secret keys should be stored in dedicated files, through secret management systems, or as environment variables.

There is no default path for age keys. Instead, they should be stored at application-specific paths. The CLI supports files where private keys are listed one per line, ignoring empty lines and lines starting with "#". These files can be parsed with ParseIdentities.

When integrating age into a new system, it's recommended that you only support X25519 keys, and not SSH keys. The latter are supported for manual encryption operations. If you need to tie into existing key management infrastructure, you might want to consider implementing your own Recipient and Identity.

Backwards compatibility

Files encrypted with a stable version (not alpha, beta, or release candidate) of age, or with any v1.0.0 beta or release candidate, will decrypt with any later versions of the v1 API. This might change in v2, in which case v1 will be maintained with security fixes for compatibility with older files.

If decrypting an older file poses a security risk, doing so might require an explicit opt-in in the API.

Index

Examples

Constants

This section is empty.

Variables

View Source
var ErrIncorrectIdentity = errors.New("incorrect identity for recipient block")

Functions

func Decrypt

func Decrypt(src io.Reader, identities ...Identity) (io.Reader, error)

Decrypt decrypts a file encrypted to one or more identities.

It returns a Reader reading the decrypted plaintext of the age file read from src. All identities will be tried until one successfully decrypts the file.

Example
package main

import (
	"bytes"
	"fmt"
	"io"
	"log"
	"os"

	"filippo.io/age"
)

// DO NOT hardcode the private key. Store it in a secret storage solution,
// on disk if the local machine is trusted, or have the user provide it.
var privateKey string

func main() {
	identity, err := age.ParseX25519Identity(privateKey)
	if err != nil {
		log.Fatalf("Failed to parse private key: %v", err)
	}

	f, err := os.Open("testdata/example.age")
	if err != nil {
		log.Fatalf("Failed to open file: %v", err)
	}

	r, err := age.Decrypt(f, identity)
	if err != nil {
		log.Fatalf("Failed to open encrypted file: %v", err)
	}
	out := &bytes.Buffer{}
	if _, err := io.Copy(out, r); err != nil {
		log.Fatalf("Failed to read encrypted file: %v", err)
	}

	fmt.Printf("File contents: %q\n", out.Bytes())
}
Output:

File contents: "Black lives matter."

func Encrypt

func Encrypt(dst io.Writer, recipients ...Recipient) (io.WriteCloser, error)

Encrypt encrypts a file to one or more recipients.

Writes to the returned WriteCloser are encrypted and written to dst as an age file. Every recipient will be able to decrypt the file.

The caller must call Close on the WriteCloser when done for the last chunk to be encrypted and flushed to dst.

Example
package main

import (
	"bytes"
	"fmt"
	"io"
	"log"

	"filippo.io/age"
)

func main() {
	publicKey := "age1cy0su9fwf3gf9mw868g5yut09p6nytfmmnktexz2ya5uqg9vl9sss4euqm"
	recipient, err := age.ParseX25519Recipient(publicKey)
	if err != nil {
		log.Fatalf("Failed to parse public key %q: %v", publicKey, err)
	}

	out := &bytes.Buffer{}

	w, err := age.Encrypt(out, recipient)
	if err != nil {
		log.Fatalf("Failed to create encrypted file: %v", err)
	}
	if _, err := io.WriteString(w, "Black lives matter."); err != nil {
		log.Fatalf("Failed to write to encrypted file: %v", err)
	}
	if err := w.Close(); err != nil {
		log.Fatalf("Failed to close encrypted file: %v", err)
	}

	fmt.Printf("Encrypted file size: %d\n", out.Len())
}
Output:

Encrypted file size: 219

Types

type Identity

type Identity interface {
	Unwrap(stanzas []*Stanza) (fileKey []byte, err error)
}

An Identity is passed to Decrypt to unwrap an opaque file key from a recipient stanza. It can be for example a secret key like X25519Identity, a plugin, or a custom implementation.

Unwrap must return an error wrapping ErrIncorrectIdentity if none of the recipient stanzas match the identity, any other error will be considered fatal.

Most age API users won't need to interact with this directly, and should instead pass Recipient implementations to Encrypt and Identity implementations to Decrypt.

func ParseIdentities

func ParseIdentities(f io.Reader) ([]Identity, error)

ParseIdentities parses a file with one or more private key encodings, one per line. Empty lines and lines starting with "#" are ignored.

This is the same syntax as the private key files accepted by the CLI, except the CLI also accepts SSH private keys, which are not recommended for the average application.

Currently, all returned values are of type *X25519Identity, but different types might be returned in the future.

Example
package main

import (
	"bytes"
	"fmt"
	"io"
	"log"
	"os"

	"filippo.io/age"
)

func main() {
	keyFile, err := os.Open("testdata/example_keys.txt")
	if err != nil {
		log.Fatalf("Failed to open private keys file: %v", err)
	}
	identities, err := age.ParseIdentities(keyFile)
	if err != nil {
		log.Fatalf("Failed to parse private key: %v", err)
	}

	f, err := os.Open("testdata/example.age")
	if err != nil {
		log.Fatalf("Failed to open file: %v", err)
	}

	r, err := age.Decrypt(f, identities...)
	if err != nil {
		log.Fatalf("Failed to open encrypted file: %v", err)
	}
	out := &bytes.Buffer{}
	if _, err := io.Copy(out, r); err != nil {
		log.Fatalf("Failed to read encrypted file: %v", err)
	}

	fmt.Printf("File contents: %q\n", out.Bytes())
}
Output:

File contents: "Black lives matter."

type NoIdentityMatchError

type NoIdentityMatchError struct {
	// Errors is a slice of all the errors returned to Decrypt by the Unwrap
	// calls it made. They all wrap ErrIncorrectIdentity.
	Errors []error
}

NoIdentityMatchError is returned by Decrypt when none of the supplied identities match the encrypted file.

func (*NoIdentityMatchError) Error

func (*NoIdentityMatchError) Error() string

type Recipient

type Recipient interface {
	Wrap(fileKey []byte) ([]*Stanza, error)
}

A Recipient is passed to Encrypt to wrap an opaque file key to one or more recipient stanza(s). It can be for example a public key like X25519Recipient, a plugin, or a custom implementation.

Most age API users won't need to interact with this directly, and should instead pass Recipient implementations to Encrypt and Identity implementations to Decrypt.

func ParseRecipients

func ParseRecipients(f io.Reader) ([]Recipient, error)

ParseRecipients parses a file with one or more public key encodings, one per line. Empty lines and lines starting with "#" are ignored.

This is the same syntax as the recipients files accepted by the CLI, except the CLI also accepts SSH recipients, which are not recommended for the average application.

Currently, all returned values are of type *X25519Recipient, but different types might be returned in the future.

type RecipientWithLabels added in v1.2.0

type RecipientWithLabels interface {
	WrapWithLabels(fileKey []byte) (s []*Stanza, labels []string, err error)
}

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 ScryptIdentity

type ScryptIdentity struct {
	// contains filtered or unexported fields
}

ScryptIdentity is a password-based identity.

func NewScryptIdentity

func NewScryptIdentity(password string) (*ScryptIdentity, error)

NewScryptIdentity returns a new ScryptIdentity with the provided password.

func (*ScryptIdentity) SetMaxWorkFactor

func (i *ScryptIdentity) SetMaxWorkFactor(logN int)

SetMaxWorkFactor sets the maximum accepted scrypt work factor to 2^logN. It must be called before Unwrap.

This caps the amount of work that Decrypt might have to do to process received files. If SetMaxWorkFactor is not called, a fairly high default is used, which might not be suitable for systems processing untrusted files.

func (*ScryptIdentity) Unwrap

func (i *ScryptIdentity) Unwrap(stanzas []*Stanza) ([]byte, error)

type ScryptRecipient

type ScryptRecipient struct {
	// contains filtered or unexported fields
}

ScryptRecipient is a password-based recipient. Anyone with the password can decrypt the message.

If a ScryptRecipient is used, it must be the only recipient for the file: it can't be mixed with other recipient types and can't be used multiple times for the same file.

Its use is not recommended for automated systems, which should prefer X25519Recipient.

func NewScryptRecipient

func NewScryptRecipient(password string) (*ScryptRecipient, error)

NewScryptRecipient returns a new ScryptRecipient with the provided password.

func (*ScryptRecipient) SetWorkFactor

func (r *ScryptRecipient) SetWorkFactor(logN int)

SetWorkFactor sets the scrypt work factor to 2^logN. It must be called before Wrap.

If SetWorkFactor is not called, a reasonable default is used.

func (*ScryptRecipient) Wrap

func (r *ScryptRecipient) Wrap(fileKey []byte) ([]*Stanza, error)

func (*ScryptRecipient) WrapWithLabels added in v1.2.0

func (r *ScryptRecipient) WrapWithLabels(fileKey []byte) (stanzas []*Stanza, labels []string, err error)

WrapWithLabels implements age.RecipientWithLabels, returning a random label. This ensures a ScryptRecipient can't be mixed with other recipients (including other ScryptRecipients).

Users reasonably expect files encrypted to a passphrase to be authenticated by that passphrase, i.e. for it to be impossible to produce a file that decrypts successfully with a passphrase without knowing it. If a file is encrypted to other recipients, those parties can produce different files that would break that expectation.

type Stanza

type Stanza struct {
	Type string
	Args []string
	Body []byte
}

A Stanza is a section of the age header that encapsulates the file key as encrypted to a specific recipient.

Most age API users won't need to interact with this directly, and should instead pass Recipient implementations to Encrypt and Identity implementations to Decrypt.

type X25519Identity

type X25519Identity struct {
	// contains filtered or unexported fields
}

X25519Identity is the standard age private key, which can decrypt messages encrypted to the corresponding X25519Recipient.

func GenerateX25519Identity

func GenerateX25519Identity() (*X25519Identity, error)

GenerateX25519Identity randomly generates a new X25519Identity.

Example
package main

import (
	"fmt"
	"log"

	"filippo.io/age"
)

func main() {
	identity, err := age.GenerateX25519Identity()
	if err != nil {
		log.Fatalf("Failed to generate key pair: %v", err)
	}

	fmt.Printf("Public key: %s...\n", identity.Recipient().String()[:4])
	fmt.Printf("Private key: %s...\n", identity.String()[:16])
}
Output:

Public key: age1...
Private key: AGE-SECRET-KEY-1...

func ParseX25519Identity

func ParseX25519Identity(s string) (*X25519Identity, error)

ParseX25519Identity returns a new X25519Identity from a Bech32 private key encoding with the "AGE-SECRET-KEY-1" prefix.

func (*X25519Identity) Recipient

func (i *X25519Identity) Recipient() *X25519Recipient

Recipient returns the public X25519Recipient value corresponding to i.

func (*X25519Identity) String

func (i *X25519Identity) String() string

String returns the Bech32 private key encoding of i.

func (*X25519Identity) Unwrap

func (i *X25519Identity) Unwrap(stanzas []*Stanza) ([]byte, error)

type X25519Recipient

type X25519Recipient struct {
	// contains filtered or unexported fields
}

X25519Recipient is the standard age public key. Messages encrypted to this recipient can be decrypted with the corresponding X25519Identity.

This recipient is anonymous, in the sense that an attacker can't tell from the message alone if it is encrypted to a certain recipient.

func ParseX25519Recipient

func ParseX25519Recipient(s string) (*X25519Recipient, error)

ParseX25519Recipient returns a new X25519Recipient from a Bech32 public key encoding with the "age1" prefix.

func (*X25519Recipient) String

func (r *X25519Recipient) String() string

String returns the Bech32 public key encoding of r.

func (*X25519Recipient) Wrap

func (r *X25519Recipient) Wrap(fileKey []byte) ([]*Stanza, error)

Directories

Path Synopsis
Package agessh provides age.Identity and age.Recipient implementations of types "ssh-rsa" and "ssh-ed25519", which allow reusing existing SSH keys for encryption with age-encryption.org/v1.
Package agessh provides age.Identity and age.Recipient implementations of types "ssh-rsa" and "ssh-ed25519", which allow reusing existing SSH keys for encryption with age-encryption.org/v1.
Package armor provides a strict, streaming implementation of the ASCII armoring format for age files.
Package armor provides a strict, streaming implementation of the ASCII armoring format for age files.
cmd
age
internal
bech32
Package bech32 is a modified version of the reference implementation of BIP173.
Package bech32 is a modified version of the reference implementation of BIP173.
format
Package format implements the age file format.
Package format implements the age file format.
stream
Package stream implements a variant of the STREAM chunked encryption scheme.
Package stream implements a variant of the STREAM chunked encryption scheme.
Package plugin implements the age plugin protocol.
Package plugin implements the age plugin protocol.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL