age

package module
v0.0.0-...-198f201 Latest Latest
Warning

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

Go to latest
Published: Jul 17, 2026 License: BSD-3-Clause Imports: 7 Imported by: 0

README

go-ruby-age/age

age — go-ruby-age

Docs License Go Coverage

A pure-Go (no cgo) reimplementation of Ruby's age gem — the binding for the age file-encryption format. It mirrors the gem's public surface (Age.encrypt / Age.decrypt, the Age::X25519 and Age::Scrypt recipient/identity types, ASCII armor, the streaming Age::Encryptor / Age::Decryptor and the Age::Error tree) and produces files the reference age CLI reads — and reads files it writes — without any Ruby runtime.

It is a sibling of go-ruby-bcrypt (OpenBSD bcrypt), go-ruby-regexp (the Onigmo engine) and go-ruby-erb (the ERB compiler), and is intended as the age backend for go-embedded-ruby.

Built on the reference implementation

The age format and its cryptography are provided by filippo.io/age — the reference age implementation written by age's author — and its age/armor package. This module does not reimplement the format or the crypto; it is a faithful, Ruby-shaped façade over it. Because the primitives (ChaCha20-Poly1305, X25519, scrypt) are endian-independent, the package is byte-identical on every supported architecture, big- or little-endian.

Ruby-faithful surface

Ruby (age gem) This package
Age.encrypt(pt, recipients:) age.Encrypt(pt, []age.Recipient{…})
Age.encrypt(pt, …, armor: true) age.Encrypt(pt, …, age.WithArmor())
Age.decrypt(ct, identities:) age.Decrypt(ct, []age.Identity{…})
Age::X25519::Identity.generate x25519.Generate()
Age::X25519::Identity#to_public (*x25519.Identity).ToPublic()
Age::X25519::Identity#to_s (*x25519.Identity).String()AGE-SECRET-KEY-1…
Age::X25519::Recipient.from_string x25519.ParseRecipient("age1…")
Age::Scrypt::Recipient.new(pass) scrypt.NewRecipient(pass) + SetWorkFactor(logN)
Age::Scrypt::Identity.new(pass) scrypt.NewIdentity(pass) + SetMaxWorkFactor(logN)
Age::Encryptor / Age::Decryptor age.Encryptor / age.Decryptor (IO wrappers)
Age::Error (base) age.Err (wrapped by every error)
NoIdentityMatchError age.ErrNoIdentityMatch
bad passphrase age.ErrIncorrectPassphrase
parse / format errors age.ErrParse / age.ErrFormat

Install

go get github.com/go-ruby-age/age

Usage

package main

import (
	"fmt"

	"github.com/go-ruby-age/age"
	"github.com/go-ruby-age/age/scrypt"
	"github.com/go-ruby-age/age/x25519"
)

func main() {
	// X25519 key pair.
	id, _ := x25519.Generate()
	fmt.Println(id.String())            // AGE-SECRET-KEY-1…
	fmt.Println(id.ToPublic().String()) // age1…

	// Encrypt to the recipient, decrypt with the identity.
	ct, _ := age.Encrypt([]byte("hello"), []age.Recipient{id.ToPublic()})
	pt, _ := age.Decrypt(ct, []age.Identity{id})
	fmt.Println(string(pt)) // hello

	// ASCII armor.
	armored, _ := age.Encrypt([]byte("hi"), []age.Recipient{id.ToPublic()}, age.WithArmor())
	fmt.Println(string(armored[:33])) // -----BEGIN AGE ENCRYPTED FILE-----

	// Passphrase (scrypt) — an scrypt recipient must be the only recipient.
	r, _ := scrypt.NewRecipient("correct horse battery staple")
	sct, _ := age.Encrypt([]byte("secret"), []age.Recipient{r})
	sid, _ := scrypt.NewIdentity("correct horse battery staple")
	spt, _ := age.Decrypt(sct, []age.Identity{sid})
	fmt.Println(string(spt)) // secret
}

Tests & coverage

The suite is self-contained and deterministic: X25519 and scrypt round-trips (binary and armored), multiple-recipient decryption, wrong-identity and wrong-passphrase rejection, tampered-ciphertext authentication failure, and the streaming wrappers — plus interop test vectors frozen from the reference age tool (a binary message, an armored message and a passphrase message, all decrypted here against a fixed key), and a check that our own output is read back by filippo.io/age directly. No Ruby, no age CLI and no network are needed, so every platform lane holds the gate at 100 % coverage.

COVERPKG=$(go list ./... | paste -sd, -)
go test -race -coverpkg="$COVERPKG" -coverprofile=cover.out ./...
go tool cover -func=cover.out | tail -1   # 100.0%

CGO-free, gofmt + go vet clean, and green across the six 64-bit Go targets (amd64, arm64, riscv64, loong64, ppc64le, s390x — the last big-endian).

License

BSD-3-Clause — see LICENSE. Copyright the go-ruby-age/age authors.

WebAssembly

Being pure Go (CGO=0), this library also compiles to WebAssembly — both GOOS=js GOARCH=wasm (browser / Node.js) and GOOS=wasip1 GOARCH=wasm (WASI). CI builds both targets on every push, alongside the six 64-bit native/qemu arches.

GOOS=js     GOARCH=wasm go build ./...   # browser / Node
GOOS=wasip1 GOARCH=wasm go build ./...   # WASI (wasmtime, wasmer, wasmedge, …)

Documentation

Overview

Package age is a pure-Go (no cgo) reimplementation of Ruby's age gem — the binding for the age file-encryption format (https://age-encryption.org).

It mirrors the gem's public surface: the Age.encrypt / Age.decrypt one-shots, the Age::X25519 and Age::Scrypt recipient/identity types, ASCII armor, the streaming Age::Encryptor / Age::Decryptor IO wrappers and the Age::Error exception tree. The format and cryptography are provided by filippo.io/age, the reference age implementation written by age's author, so files produced here are read by the reference age CLI and vice-versa — without any Ruby runtime.

Quick start

id, _ := x25519.Generate()
ct, _ := age.Encrypt([]byte("hello"), []age.Recipient{id.ToPublic()})
pt, _ := age.Decrypt(ct, []age.Identity{id})
string(pt) // "hello"

The age format and its ChaCha20-Poly1305 / X25519 / scrypt primitives are endian-independent, so the package is byte-identical on every supported architecture, big- or little-endian.

Index

Constants

View Source
const Version = "0.1.0"

Version is the version of this Go port. It is independent of the upstream Ruby age gem's version.

Variables

View Source
var (
	// Err is the base sentinel every other error in the package wraps, mirroring
	// the gem's Age::Error base class.
	Err = errors.New("age")

	// ErrNoIdentityMatch is returned by Decrypt when none of the supplied
	// identities could unwrap the file key, mirroring the gem's
	// Age::NoMatchingKeys (age's NoIdentityMatchError).
	ErrNoIdentityMatch = fmt.Errorf("%w: no identity matched any recipient stanza", Err)

	// ErrIncorrectPassphrase is returned by Decrypt when a passphrase-encrypted
	// (scrypt) file is decrypted with the wrong passphrase.
	ErrIncorrectPassphrase = fmt.Errorf("%w: incorrect passphrase", Err)

	// ErrParse is returned when a key string, recipient or passphrase cannot be
	// parsed, mirroring the gem's parse/format errors.
	ErrParse = fmt.Errorf("%w: parse error", Err)

	// ErrFormat is returned when a ciphertext is malformed, truncated or has been
	// tampered with (authentication failure), mirroring the gem's decrypt error.
	ErrFormat = fmt.Errorf("%w: malformed or corrupt ciphertext", Err)

	// ErrEncrypt is returned when encryption cannot proceed, for example when no
	// recipients are given or an scrypt recipient is combined with another
	// recipient (age requires an scrypt recipient to be the only one).
	ErrEncrypt = fmt.Errorf("%w: encryption failed", Err)
)

The package's sentinel errors mirror the exception hierarchy of the Ruby age gem, whose classes descend from Age::Error. Every error returned by the package wraps Err, so a caller can match the whole family with errors.Is(err, age.Err) — the Go equivalent of "rescue Age::Error" — or match a specific member for finer control.

Functions

func Decrypt

func Decrypt(ciphertext []byte, identities []Identity) ([]byte, error)

Decrypt decrypts an age message (binary or ASCII-armored — the framing is detected automatically) using the given identities and returns the plaintext, mirroring the gem's Age.decrypt(ciphertext, identities:). It returns ErrNoIdentityMatch when no identity matches, ErrIncorrectPassphrase for a wrong scrypt passphrase and ErrFormat when the message is corrupt or has been tampered with.

func Encrypt

func Encrypt(plaintext []byte, recipients []Recipient, opts ...Option) ([]byte, error)

Encrypt encrypts plaintext to the given recipients and returns the complete age message, mirroring the gem's Age.encrypt(plaintext, recipients:). With WithArmor the output is ASCII-armored. Any identity holding a private key for one of the recipients can later Decrypt it.

Types

type Decryptor

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

Decryptor streams decryption from an underlying io.Reader, mirroring the gem's Age::Decryptor. Binary and ASCII-armored input are detected automatically. It implements io.Reader.

func NewDecryptor

func NewDecryptor(src io.Reader, identities []Identity) (*Decryptor, error)

NewDecryptor begins decrypting the age message read from src with the given identities, mirroring Age::Decryptor.new. It fails immediately with ErrNoIdentityMatch / ErrIncorrectPassphrase when the header cannot be unwrapped, or ErrFormat when the header is malformed.

func (*Decryptor) Read

func (d *Decryptor) Read(p []byte) (int, error)

Read decrypts into p. A tampered or truncated payload surfaces here as an ErrFormat authentication failure.

type Encryptor

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

Encryptor streams encryption to an underlying io.Writer, mirroring the gem's Age::Encryptor. Write encrypted chunks with Write and finish the message with Close (which flushes the final chunk and, when armored, the trailing footer); Close must be called for the output to be valid. It implements io.WriteCloser.

func NewEncryptor

func NewEncryptor(dst io.Writer, recipients []Recipient, opts ...Option) (*Encryptor, error)

NewEncryptor starts an age message on dst encrypted to the given recipients, mirroring Age::Encryptor.new. With WithArmor the message is ASCII-armored.

func (*Encryptor) Close

func (e *Encryptor) Close() error

Close flushes the final chunk and, for an armored stream, the ASCII footer. It must be called to produce a valid message.

func (*Encryptor) Write

func (e *Encryptor) Write(p []byte) (int, error)

Write encrypts and writes p. Written data is buffered into fixed-size chunks and only guaranteed to reach the underlying writer once Close is called.

type Identity

type Identity = fage.Identity

Identity is a key that can decrypt a message (an age identity). The X25519 and scrypt identity types satisfy it, mirroring the objects passed to the gem's Age.decrypt(identities:) keyword.

type Option

type Option func(*options)

Option configures Encrypt / NewEncryptor. It mirrors the keyword arguments of the gem's Age.encrypt.

func WithArmor

func WithArmor() Option

WithArmor selects PEM-style ASCII "armor" output, wrapping the binary age message in "-----BEGIN AGE ENCRYPTED FILE-----" … "-----END AGE ENCRYPTED FILE-----". It mirrors Age.encrypt(..., armor: true).

type Recipient

type Recipient = fage.Recipient

Recipient is a party a message can be encrypted to (an age recipient). The X25519 and scrypt recipient types satisfy it, mirroring the objects passed to the gem's Age.encrypt(recipients:) keyword.

Directories

Path Synopsis
Package scrypt mirrors the gem's Age::Scrypt namespace: passphrase-based age recipients and identities.
Package scrypt mirrors the gem's Age::Scrypt namespace: passphrase-based age recipients and identities.
Package x25519 mirrors the gem's Age::X25519 namespace: native age X25519 key-pair recipients and identities, whose string forms are the familiar "age1…" recipients and "AGE-SECRET-KEY-1…" identities.
Package x25519 mirrors the gem's Age::X25519 namespace: native age X25519 key-pair recipients and identities, whose string forms are the familiar "age1…" recipients and "AGE-SECRET-KEY-1…" identities.

Jump to

Keyboard shortcuts

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