72 Commits
Author SHA1 Message Date
Filippo Valsorda b6b5f4300f cmd/age: disable golang/go#61779 tests workaround 2024-06-16 13:52:42 +02:00
Filippo Valsorda 627e6bc9d8 cmd/age: create file for empty decryptions
Fixes #555
Updates #159
Updates #57
2024-06-16 13:50:52 +02:00
Filippo Valsorda 7ed486868a .github/workflows: apparently setup-go has no defaults 2024-06-16 06:03:13 -04:00
Filippo Valsorda 2a761fcb8c .github/workflows: update GitHub Actions 2024-06-16 06:03:13 -04:00
Filippo Valsorda 98e7afcbac all: upgrade dependencies 2024-06-16 06:03:13 -04:00
Filippo Valsorda 5ef63b6153 .github/workflows: install bootstrap Go 2024-06-16 06:03:13 -04:00
Filippo Valsorda bc21ece498 .github/workflows: disable environment
It's very spammy, just move the secret to a repository secret.
2024-06-16 06:03:13 -04:00
Filippo Valsorda 69c21b83fb README: fix scoop command
Closes #564
2024-06-16 05:06:58 -04:00
Filippo Valsorda 35cf02b1d0 .github: link maintenance policy from CONTRIBUTING.md 2024-06-16 05:03:03 -04:00
Jakub Wilk 29b68c20fc README: fix typo (#534) 2024-01-10 06:40:17 -05:00
Filippo Valsorda 101cc86763 README: Debian 12 installation instructions 2023-09-20 08:41:00 -04:00
Filippo Valsorda 6ad4560f4a .github/workflows: drop FreeBSD tests
This is unfortunate, but without a live platform to test on,
I can't investigate issues, and CI is now failing with just

   ?   	filippo.io/age/cmd/age-keygen	[no test files]
  Killed

which really could be anything.
2023-08-07 18:44:57 -04:00
Filippo Valsorda 93055632ad cmd/age: fix FreeBSD tests 2023-08-06 19:39:31 +02:00
Filippo Valsorda 294b0aa1e3 plugin: skip execution tests on Windows for now 2023-08-06 19:03:27 +02:00
Filippo Valsorda f1f96c25e0 plugin: build tag EncodeX25519Recipient which uses crypto/ecdh 2023-08-06 18:36:38 +02:00
Filippo Valsorda 9fd564d543 .github/workflows: update and fix CI 2023-08-06 18:29:16 +02:00
Filippo Valsorda c89f0b932e age,plugin: add RecipientWithLabels 2023-08-05 21:34:47 +02:00
Filippo Valsorda dd733c5c0f cmd/age: grease the client-controlled plugin phases 2023-08-05 21:34:14 +02:00
Filippo Valsorda 004b544d83 plugin: add EncodeX25519Recipient 2023-08-05 21:34:14 +02:00
Filippo Valsorda 02181d83e9 plugin: add identity and recipient encoding 2023-08-05 21:34:14 +02:00
Filippo Valsorda 6976c5fca5 plugin: expose package 2023-08-05 21:34:14 +02:00
Filippo Valsorda 980763a16e age: make TestVectorsRoundTrip a little stricter 2023-07-23 00:54:40 +02:00
Filippo Valsorda 4740a92ef9 age: use testkit vectors to test armor, header, and STREAM round-trips
Before

	filippo.io/age/armor	coverage: 72.3% of statements in filippo.io/age/...
	filippo.io/age/internal/format	coverage: 86.8% of statements in filippo.io/age/...
	filippo.io/age/internal/stream	coverage: 83.9% of statements in filippo.io/age/...

After

	filippo.io/age/armor	coverage: 88.0% of statements in filippo.io/age/...
	filippo.io/age/internal/format	coverage: 87.6% of statements in filippo.io/age/...
	filippo.io/age/internal/stream	coverage: 86.0% of statements in filippo.io/age/...
2023-07-23 00:18:41 +02:00
Stepan 6c36e167c8 README: update release download link (#512) 2023-06-30 09:05:44 -04:00
Filippo Valsorda 9f0a2d25ac README: add link to awesome age list 2023-04-22 16:40:45 -04:00
Helio Machado b6537b1865 .github/workflows: trigger interop. tests using gh (#481) 2023-04-22 06:01:29 -04:00
GitHub Actions 486b6dac96 doc: regenerate groff and html man pages 2023-04-22 09:53:44 +00:00
zhsj 877ca247e3 .github/workflows: update ronn to 0.9 (#483)
This fixes apostrophes in generated manpage.
Apostrophes should be entered as `'` ; using `\'` produces an acute accent.

ronn in Ubuntu is from https://github.com/apjanke/ronn-ng
2023-04-22 05:53:03 -04:00
andros21 502b180b17 README: dark/light mode logo (#500)
https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#specifying-the-theme-an-image-is-shown-to
2023-04-22 05:46:50 -04:00
Filippo Valsorda 8e3f74c283 cmd/age: deflake TestScript and update testscript 2023-01-02 13:34:35 +01:00
Berk D. Demir edf7388f77 age: depend on c2sp.org/CCTV/age for TestVectors
Simplifies importing test data from CCTV without needing to invoke
"go mod download" from TestVectors. Makes life easier for package
builders with no networking, like Nixpkgs.
2022-12-30 18:24:08 -05:00
Filippo Valsorda 5471e05672 Revert "all: temporarily disable testscript tests"
This reverts commit 90a446549a.
2022-12-29 21:53:31 +01:00
Filippo Valsorda c6dcfa1efc all: temporarily disable testscript tests
They require a replace directive that breaks "go install". Will revert
this after tagging a new latest release.
2022-12-26 15:36:58 -05:00
Filippo Valsorda a1fabee4c8 all: upgrade dependencies 2022-12-26 15:36:58 -05:00
Filippo Valsorda 7354aa0d08 internal/bech32: remove data length limit
Fixes #453
2022-12-23 12:28:06 +01:00
Filippo Valsorda bf8d2a3911 age: move testkit to CCTV 2022-10-28 20:11:09 +02:00
Helio Machado 5d5c9c48d8 .github/workflows: parallelize build process (#456) 2022-10-10 07:41:06 -04:00
Helio Machado e05ce267a6 .github/workflows: use gh to upload release artifacts (#455)
* .github/workflows: use gh to upload release artifacts

https://cli.github.com/manual/gh_release_upload

* .github/workflows: remove unnecessary braces

When you use expressions in an if conditional, you may omit the expression syntax ${{ }} because GitHub automatically evaluates the if conditional as an expression.

https://docs.github.com/en/actions/learn-github-actions/expressions
2022-10-09 22:38:36 -04:00
GitHub Actions 000e931101 doc: regenerate groff and html man pages 2022-09-28 09:47:53 +00:00
a1346054 ebf48f1bbc doc: trim trailing whitespace 2022-09-28 05:46:34 -04:00
a1346054 3f2209cab9 doc: fix typos 2022-09-28 05:46:34 -04:00
Filippo Valsorda bbac0a501c README: add a couple links to the heading 2022-09-26 08:16:06 -04:00
Mehrnoosh Beniss 02a01c6a6f README: switch order of installation and usage (#448) 2022-09-26 08:10:01 -04:00
Filippo Valsorda 8328d19d3e .github/workflows: update Go and FreeBSD in test 2022-09-13 13:18:49 +02:00
Simone RagusaandFilippo Valsorda 1f84a45175 .github/workflows: change Go LICENSE URL from golang.org to go.dev (#449)
Co-authored-by: Filippo Valsorda <github@bip.filippo.io>
2022-09-11 15:54:32 -04:00
Filippo Valsorda 084c974f53 all: use better implementation of testscript timeouts 2022-07-28 14:21:43 +02:00
Filippo Valsorda 36ae5671cf cmd/age: buffer armored ciphertext before prompting
A partial solution, still missing bracketed paste support.

Updates #364
2022-07-12 22:30:19 +02:00
Filippo Valsorda 8a02f4801f cmd/age: improve terminal-related testscripts 2022-07-12 22:26:51 +02:00
Filippo Valsorda e41463e117 cmd/age: fix autogenerated password log line
The newline was dropped in 0ab5c73.
2022-07-12 22:26:51 +02:00
Filippo Valsorda f66877cfa5 README: update installation instructions to provide v1.0.0+
Updates #371
2022-07-07 12:11:02 -04:00
Filippo Valsorda 891be91d42 armor: add FuzzMalleability test 2022-07-03 12:48:48 +02:00
Filippo Valsorda a5d38ae6ce armor: correct whitespace and trailing garbage handling
Ignore whitespace before and after file, reject trailing garbage and
whitespace within the file.
2022-07-03 12:48:48 +02:00
Filippo Valsorda 799c2bf8e8 tests: add armor tests 2022-07-03 12:48:48 +02:00
Filippo Valsorda e84d74239e age: wrap decryption errors through and add armor.Error 2022-07-03 12:48:48 +02:00
Filippo Valsorda 95ba0188d1 age: keep age lowercase in all docs 2022-07-03 12:48:48 +02:00
Filippo Valsorda 0ab5c738fb cmd/age: ensure TUI output goes all to the terminal 2022-07-03 12:48:48 +02:00
Filippo Valsorda de7c1fb565 agessh: support PKCS#8-encoded Ed25519 private keys
OpenSSH never generated them (unencrypted, and golang.org/x/crypto/ssh
doesn't support encrypted PKCS#8 for now, so the encrypted_keys.go
change is technically superfluous) but there are other systems that
produce them (for example, 1Password). Unfortunately, ParseRawPrivateKey
returns a value type for PKCS#8 and a pointer type for the OpenSSH
format (golang/go#51974), so we need to handle both.

Fixes #429
2022-07-03 12:48:48 +02:00
Filippo Valsorda 92fb4d508c agessh: properly detect public key mismatch for encrypted keys 2022-07-03 12:48:48 +02:00
Filippo Valsorda 331b242a9c cmd/age: add scrypt testscript 2022-07-03 12:48:48 +02:00
Filippo Valsorda c50f1ae2e1 cmd/age: replace tests with testscript 2022-06-21 12:41:38 +01:00
Filippo Valsorda 8023f06ce2 all: build tag tests that require Go 1.18 2022-06-20 22:33:31 +01:00
Filippo Valsorda 4f7bb44386 internal/format: add malleability fuzz test 2022-06-20 01:18:48 +01:00
Filippo Valsorda 3f56ac13fb internal/stream: reject trailing data (no EOF) after end of stream 2022-06-19 23:11:14 +02:00
Filippo Valsorda 2e090545df age: reject leading zeroes and sign in scrypt work factor 2022-06-19 17:52:30 +02:00
Filippo Valsorda 2088adf268 tests: add expected no match and minor additions 2022-06-19 00:11:23 +02:00
Filippo Valsorda eaa4e03cfe tests: finish parsing and X25519 tests, distinguish HMAC errors
If the implementation re-encodes the header before checking the HMAC,
that would mask malleability issues: the HMAC check would fail because
the tests HMAC'd the original header, but an attacker could also produce
the right HMAC. Instead of duplicating every parsing tests (with the
original and re-encoded HMAC), we make the test framework distinguish
HMAC errors, which ensures bad encodings are recognized as such and not
bypassable HMAC errors.
2022-06-18 13:47:00 +02:00
Filippo Valsorda f8a121dd87 tests: add more tests for hmac and X25519 encodings 2022-06-16 15:55:29 +02:00
Filippo Valsorda bb4493a7cd tests: add X25519 low order point tests 2022-06-16 11:47:27 +02:00
Filippo Valsorda 92713afd1e tests: add scrypt tests and move Go files from testdata 2022-06-15 20:38:59 +02:00
Filippo Valsorda 787044bdb6 testdata: convert last chunk tests to testkit 2022-06-15 19:38:18 +02:00
Filippo Valsorda a787511e01 testdata: add a few more testkit vectors 2022-06-15 18:54:09 +02:00
Filippo Valsorda e49b1f9afe internal/testkit: new test framework 2022-06-15 18:53:07 +02:00
72 changed files with 1824 additions and 807 deletions

No files matched your search

+1
View File
@@ -1 +1,2 @@
*.age binary
testdata/testkit/* binary
+4 -2
View File
@@ -12,6 +12,8 @@ age is a little unusual in how it is maintained. I like to keep the code style c
Therefore, **be prepared for your change to get reimplemented rather than merged**, and please don't be offended if that happens. PRs are still appreciated as a way to clarify the intended behavior, but are not at all required: prefer focusing on providing detailed context in an issue report instead.
To learn more, please see my [maintenance policy](https://github.com/FiloSottile/FiloSottile/blob/main/maintenance.md).
<!-- ## Feature requests
age is small, simple, and config-free by design. A lot of effort is put into resisting scope creep and enabling use cases by integrating and interoperating well with other projects rather than by adding features.
@@ -22,11 +24,11 @@ In particular, I'm unlikely to merge into the main repo anything I don't use mys
age itself is not community maintained, but its ecosystem very much is, and that's where a lot of the strength of age is! Here are some ideas for ways to contribute to age and its ecosystem, besides contributing to this repository.
* **Write an article about how to use age for a certain community or use case.** The number one reason people don't use age is because they haven't heard about it and existsing tutorials present more complex alternatives.
* **Write an article about how to use age for a certain community or use case.** The number one reason people don't use age is because they haven't heard about it and existing tutorials present more complex alternatives.
* Integrate age into existing projects that might use it, for example replacing legacy alternatives.
* Build and maintain an [age plugin](https://c2sp.org/age-plugin) for a KMS or platform.
* Watch the [discussions](https://github.com/FiloSottile/age/discussions) and help other users.
* Provide bindings in a langauge or framework that doesn't support age well.
* Provide bindings in a language or framework that doesn't support age well.
* Package age for an ecosystem that doesn't have packages yet.
If you build or write something related to age, [let me know](https://github.com/FiloSottile/age/discussions/new?category=general)! 💖
+43 -58
View File
@@ -10,53 +10,57 @@ jobs:
build:
name: Build binaries
runs-on: ubuntu-latest
environment: "Build, sign, release binaries"
strategy:
matrix:
include:
- {GOOS: linux, GOARCH: amd64}
- {GOOS: linux, GOARCH: arm, GOARM: 6}
- {GOOS: linux, GOARCH: arm64}
- {GOOS: darwin, GOARCH: amd64}
- {GOOS: darwin, GOARCH: arm64}
- {GOOS: windows, GOARCH: amd64}
- {GOOS: freebsd, GOARCH: amd64}
steps:
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: 1.x
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Build binaries
- name: Build binary
run: |
sudo apt-get update && sudo apt-get install -y osslsigncode
cp LICENSE "$RUNNER_TEMP/LICENSE"
echo -e "\n---\n" >> "$RUNNER_TEMP/LICENSE"
curl "https://golang.org/LICENSE?m=text" >> "$RUNNER_TEMP/LICENSE"
curl -L "https://go.dev/LICENSE?m=text" >> "$RUNNER_TEMP/LICENSE"
VERSION="$(git describe --tags)"
function build_age() {
DIR="$(mktemp -d)"
mkdir "$DIR/age"
cp "$RUNNER_TEMP/LICENSE" "$DIR/age"
go build -o "$DIR/age" -ldflags "-X main.Version=$VERSION" -trimpath ./cmd/...
if [ "$GOOS" == "windows" ]; then
if [ -n "${{ secrets.SIGN_PASS }}" ]; then
for exe in "$DIR"/age/*.exe; do
/usr/bin/osslsigncode sign -t "http://timestamp.comodoca.com" \
-certs .github/workflows/certs/uitacllc.crt \
-key .github/workflows/certs/uitacllc.key \
-pass "${{ secrets.SIGN_PASS }}" \
-n age -in "$exe" -out "$exe.signed"
mv "$exe.signed" "$exe"
done
fi
( cd "$DIR"; zip age.zip -r age )
mv "$DIR/age.zip" "age-$VERSION-$GOOS-$GOARCH.zip"
else
tar -cvzf "age-$VERSION-$GOOS-$GOARCH.tar.gz" -C "$DIR" age
DIR="$(mktemp -d)"
mkdir "$DIR/age"
cp "$RUNNER_TEMP/LICENSE" "$DIR/age"
go build -o "$DIR/age" -ldflags "-X main.Version=$VERSION" -trimpath ./cmd/...
if [ "$GOOS" == "windows" ]; then
sudo apt-get update && sudo apt-get install -y osslsigncode
if [ -n "${{ secrets.SIGN_PASS }}" ]; then
for exe in "$DIR"/age/*.exe; do
/usr/bin/osslsigncode sign -t "http://timestamp.comodoca.com" \
-certs .github/workflows/certs/uitacllc.crt \
-key .github/workflows/certs/uitacllc.key \
-pass "${{ secrets.SIGN_PASS }}" \
-n age -in "$exe" -out "$exe.signed"
mv "$exe.signed" "$exe"
done
fi
}
export CGO_ENABLED=0
GOOS=linux GOARCH=amd64 build_age
GOOS=linux GOARCH=arm GOARM=6 build_age
GOOS=linux GOARCH=arm64 build_age
GOOS=darwin GOARCH=amd64 build_age
GOOS=darwin GOARCH=arm64 build_age
GOOS=windows GOARCH=amd64 build_age
GOOS=freebsd GOARCH=amd64 build_age
( cd "$DIR"; zip age.zip -r age )
mv "$DIR/age.zip" "age-$VERSION-$GOOS-$GOARCH.zip"
else
tar -cvzf "age-$VERSION-$GOOS-$GOARCH.tar.gz" -C "$DIR" age
fi
env:
CGO_ENABLED: 0
GOOS: ${{ matrix.GOOS }}
GOARCH: ${{ matrix.GOARCH }}
GOARM: ${{ matrix.GOARM }}
- name: Upload workflow artifacts
uses: actions/upload-artifact@v2
with:
@@ -64,7 +68,7 @@ jobs:
path: age-*
upload:
name: Upload release binaries
if: ${{ github.event_name == 'release' }}
if: github.event_name == 'release'
needs: build
permissions:
contents: write
@@ -75,26 +79,7 @@ jobs:
with:
name: age-binaries
- name: Upload release artifacts
uses: actions/github-script@v3
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const fs = require("fs").promises;
const { repo: { owner, repo }, sha } = context;
const release = await github.repos.getReleaseByTag({
owner, repo,
tag: process.env.GITHUB_REF.replace("refs/tags/", ""),
});
console.log("Release:", { release });
for (let file of await fs.readdir(".")) {
if (!file.startsWith("age-")) continue;
console.log("Uploading", file);
await github.repos.uploadReleaseAsset({
owner, repo,
release_id: release.data.id,
name: file,
data: await fs.readFile(file),
});
}
run: gh release upload "$GITHUB_REF_NAME" age-*
env:
GH_REPO: ${{ github.repository }}
GH_TOKEN: ${{ github.token }}
+6 -5
View File
@@ -8,8 +8,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Trigger interoperability tests in str4d/rage
run: |
curl -X POST https://api.github.com/repos/str4d/rage/dispatches \
-H 'Accept: application/vnd.github.v3+json' \
-H 'Authorization: token ${{ secrets.RAGE_INTEROP_ACCESS_TOKEN }}' \
--data '{"event_type": "age-interop-request", "client_payload": { "sha": "'"$GITHUB_SHA"'" }}'
run: >
gh api repos/str4d/rage/dispatches
--field event_type="age-interop-request"
--field client_payload[sha]="$GITHUB_SHA"
env:
GITHUB_TOKEN: ${{ secrets.RAGE_INTEROP_ACCESS_TOKEN }}
+5 -5
View File
@@ -6,7 +6,6 @@ on:
paths:
- '**.ronn'
- '**/ronn.yml'
- '**/ronn/**'
permissions:
contents: read
jobs:
@@ -15,10 +14,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Install ronn
run: sudo apt-get update && sudo apt-get install -y ronn
- name: Run ronn
uses: ./.github/workflows/ronn
id: ronn
run: bash -O globstar -c 'ronn **/*.ronn'
- name: Undo email mangling
# rdiscount randomizes the output for no good reason, which causes
# changes to always get committed. Sigh.
@@ -43,7 +43,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Download generated files
uses: actions/download-artifact@v2
with:
-8
View File
@@ -1,8 +0,0 @@
FROM ruby:3.0.1-buster
RUN apt-get update && apt-get install -y groff
RUN bundle config --global frozen 1
COPY Gemfile Gemfile.lock ./
RUN bundle install
ENTRYPOINT ["bash", "-O", "globstar", "-c", \
"/usr/local/bundle/bin/ronn **/*.ronn"]
-5
View File
@@ -1,5 +0,0 @@
# frozen_string_literal: true
source "https://rubygems.org"
gem "ronn", "~> 0.7.3"
-20
View File
@@ -1,20 +0,0 @@
GEM
remote: https://rubygems.org/
specs:
hpricot (0.8.6)
mustache (1.1.1)
rdiscount (2.2.0.2)
ronn (0.7.3)
hpricot (>= 0.8.2)
mustache (>= 0.7.0)
rdiscount (>= 1.5.8)
PLATFORMS
aarch64-linux
x86_64-linux
DEPENDENCIES
ronn (~> 0.7.3)
BUNDLED WITH
2.2.15
-4
View File
@@ -1,4 +0,0 @@
name: Ronn
runs:
using: docker
image: Dockerfile
+8 -21
View File
@@ -8,37 +8,20 @@ jobs:
strategy:
fail-fast: false
matrix:
go: [1.16.x, 1.17.x]
go: [1.19.x, 1.x]
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Install Go ${{ matrix.go }}
uses: actions/setup-go@v2
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go }}
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Run tests
run: go test -race ./...
freebsd:
name: Test (FreeBSD)
runs-on: macos-10.15
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Run tests
# Unpinned Action allowed with read-only permissions.
uses: vmactions/freebsd-vm@v0.1.5
with:
prepare: |
freebsd-version
pkg install -y go
go version
run: go test -buildvcs=false -race ./...
gotip:
name: Test (Go tip)
strategy:
@@ -47,6 +30,10 @@ jobs:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Install bootstrap Go
uses: actions/setup-go@v5
with:
go-version: 1.22
- name: Install Go tip (UNIX)
if: runner.os != 'Windows'
run: |
@@ -60,7 +47,7 @@ jobs:
cd $HOME/gotip/src && ./make.bat
echo "$HOME/gotip/bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v4
with:
fetch-depth: 0
- run: go version
+135 -116
View File
@@ -1,7 +1,13 @@
<p align="center"><img alt="The age logo, an wireframe of St. Peters dome in Rome, with the text: age, file encryption" width="600" src="https://user-images.githubusercontent.com/1225294/132245842-fda4da6a-1cea-4738-a3da-2dc860861c98.png"></p>
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/FiloSottile/age/blob/main/logo/logo_white.svg">
<source media="(prefers-color-scheme: light)" srcset="https://github.com/FiloSottile/age/blob/main/logo/logo.svg">
<img alt="The age logo, a wireframe of St. Peters dome in Rome, with the text: age, file encryption" width="600" src="https://github.com/FiloSottile/age/blob/main/logo/logo.svg">
</picture>
</p>
[![Go Reference](https://pkg.go.dev/badge/filippo.io/age.svg)](https://pkg.go.dev/filippo.io/age)
[![man page](https://img.shields.io/badge/age(1)-man%20page-lightgrey)](https://filippo.io/age/age.1)
[![man page](<https://img.shields.io/badge/age(1)-man%20page-lightgrey>)](https://filippo.io/age/age.1)
[![C2SP specification](https://img.shields.io/badge/%C2%A7%23-specification-blueviolet)](https://age-encryption.org/v1)
age is a simple, modern and secure file encryption tool, format, and Go library.
@@ -15,11 +21,135 @@ $ tar cvz ~/data | age -r age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9
$ age --decrypt -i key.txt data.tar.gz.age > data.tar.gz
```
The format specification is at [age-encryption.org/v1](https://age-encryption.org/v1). age was designed by [@Benjojo12](https://twitter.com/Benjojo12) and [@FiloSottile](https://twitter.com/FiloSottile).
📜 The format specification is at [age-encryption.org/v1](https://age-encryption.org/v1). age was designed by [@Benjojo12](https://twitter.com/Benjojo12) and [@FiloSottile](https://twitter.com/FiloSottile).
An alternative interoperable Rust implementation is available at [github.com/str4d/rage](https://github.com/str4d/rage).
📬 Follow the maintenance of this project by subscribing to [Maintainer Dispatches](https://filippo.io/newsletter)!
The author pronounces it `[aɡe̞]`, like the Italian [“aghe”](https://translate.google.com/?sl=it&text=aghe).
🦀 An alternative interoperable Rust implementation is available at [github.com/str4d/rage](https://github.com/str4d/rage).
🔑 Hardware PIV tokens such as YubiKeys are supported through the [age-plugin-yubikey](https://github.com/str4d/age-plugin-yubikey) plugin.
✨ For more plugins, implementations, tools, and integrations, check out the [awesome age](https://github.com/FiloSottile/awesome-age) list.
💬 The author pronounces it `[aɡe̞]` [with a hard *g*](https://translate.google.com/?sl=it&text=aghe), like GIF, and is always spelled lowercase.
## Installation
<table>
<tr>
<td>Homebrew (macOS or Linux)</td>
<td>
<code>brew install age</code>
</td>
</tr>
<tr>
<td>MacPorts</td>
<td>
<code>port install age</code>
</td>
</tr>
<tr>
<td>Alpine Linux v3.15+</td>
<td>
<code>apk add age</code>
</td>
</tr>
<tr>
<td>Arch Linux</td>
<td>
<code>pacman -S age</code>
</td>
</tr>
<tr>
<td>Debian 12+ (Bookworm)</td>
<td>
<code>apt install age</code>
</td>
</tr>
<tr>
<td>Debian 11 (Bullseye)</td>
<td>
<code>apt install age/bullseye-backports</code>
(<a href="https://backports.debian.org/Instructions/#index2h2">enable backports</a> for age v1.0.0+)
</td>
</tr>
<tr>
<td>Fedora 33+</td>
<td>
<code>dnf install age</code>
</td>
</tr>
<tr>
<td>Gentoo Linux</td>
<td>
<code>emerge app-crypt/age</code>
</td>
</tr>
<tr>
<td>NixOS / Nix</td>
<td>
<code>nix-env -i age</code>
</td>
</tr>
<tr>
<td>openSUSE Tumbleweed</td>
<td>
<code>zypper install age</code>
</td>
</tr>
<tr>
<td>Ubuntu 22.04+</td>
<td>
<code>apt install age</code>
</td>
</tr>
<tr>
<td>Void Linux</td>
<td>
<code>xbps-install age</code>
</td>
</tr>
<tr>
<td>FreeBSD</td>
<td>
<code>pkg install age</code> (security/age)
</td>
</tr>
<tr>
<td>OpenBSD 6.7+</td>
<td>
<code>pkg_add age</code> (security/age)
</td>
</tr>
<tr>
<td>Chocolatey (Windows)</td>
<td>
<code>choco install age.portable</code>
</td>
</tr>
<tr>
<td>Scoop (Windows)</td>
<td>
<code>scoop bucket add extras && scoop install age</code>
</td>
</tr>
</table>
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](https://go.dev/dl/), you can build from source.
```
go install filippo.io/age/cmd/...@latest
```
Help from new packagers is very welcome.
## Usage
@@ -133,114 +263,3 @@ $ 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.
## Installation
<table>
<tr>
<td>Homebrew (macOS or Linux)</td>
<td>
<code>brew install age</code>
</td>
</tr>
<tr>
<td>MacPorts</td>
<td>
<code>port install age</code>
</td>
</tr>
<tr>
<td>Alpine Linux v3.15+</td>
<td>
<code>apk add age</code>
</td>
</tr>
<tr>
<td>Arch Linux</td>
<td>
<code>pacman -S age</code>
</td>
</tr>
<tr>
<td>Debian 11+ (Bullseye)</td>
<td>
<code>apt install age</code>
</td>
</tr>
<tr>
<td>Fedora 33+</td>
<td>
<code>dnf install age</code>
</td>
</tr>
<tr>
<td>Gentoo Linux</td>
<td>
<code>emerge app-crypt/age</code>
</td>
</tr>
<tr>
<td>NixOS / Nix</td>
<td>
<code>nix-env -i age</code>
</td>
</tr>
<tr>
<td>openSUSE Tumbleweed</td>
<td>
<code>zypper install age</code>
</td>
</tr>
<tr>
<td>Ubuntu 21.04+</td>
<td>
<code>apt install age</code>
</td>
</tr>
<tr>
<td>Void Linux</td>
<td>
<code>xbps-install age</code>
</td>
</tr>
<tr>
<td>FreeBSD</td>
<td>
<code>pkg install age</code> (security/age)
</td>
</tr>
<tr>
<td>OpenBSD 6.7+</td>
<td>
<code>pkg_add age</code> (security/age)
</td>
</tr>
<tr>
<td>Chocolatey (Windows)</td>
<td>
<code>choco install age.portable</code>
</td>
</tr>
<tr>
<td>Scoop (Windows)</td>
<td>
<code>scoop bucket add extras; scoop install age</code>
</td>
</tr>
</table>
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.0.0-rc.1?for=darwin/arm64
...
```
If your system has [a supported version of Go](https://go.dev/dl/), you can build from source.
```
go install filippo.io/age/cmd/...@latest
```
Help from new packagers is very welcome.
+50 -17
View File
@@ -10,12 +10,12 @@
// 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,
// age encrypted files are binary and not malleable. For encoding them as text,
// use the filippo.io/age/armor package.
//
// Key management
// # Key management
//
// Age does not have a global keyring. Instead, since age keys are small,
// 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.
//
@@ -34,7 +34,7 @@
// infrastructure, you might want to consider implementing your own Recipient
// and Identity.
//
// Backwards compatibility
// # 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
@@ -51,6 +51,7 @@ import (
"errors"
"fmt"
"io"
"sort"
"filippo.io/age/internal/format"
"filippo.io/age/internal/stream"
@@ -84,6 +85,21 @@ type Recipient interface {
Wrap(fileKey []byte) ([]*Stanza, 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 RecipientWithLabels interface {
WrapWithLabels(fileKey []byte) (s []*Stanza, labels []string, err error)
}
// A Stanza is a section of the age header that encapsulates the file key as
// encrypted to a specific recipient.
//
@@ -111,27 +127,24 @@ func Encrypt(dst io.Writer, recipients ...Recipient) (io.WriteCloser, error) {
return nil, errors.New("no recipients specified")
}
// As a best effort, prevent an API user from generating a file that the
// ScryptIdentity will refuse to decrypt. This check can't unfortunately be
// implemented as part of the Recipient interface, so it lives as a special
// case in Encrypt.
for _, r := range recipients {
if _, ok := r.(*ScryptRecipient); ok && len(recipients) != 1 {
return nil, errors.New("an ScryptRecipient must be the only one for the file")
}
}
fileKey := make([]byte, fileKeySize)
if _, err := rand.Read(fileKey); err != nil {
return nil, err
}
hdr := &format.Header{}
var labels []string
for i, r := range recipients {
stanzas, err := r.Wrap(fileKey)
stanzas, l, err := wrapWithLabels(r, fileKey)
if err != nil {
return nil, fmt.Errorf("failed to wrap key for recipient #%d: %v", i, err)
}
sort.Strings(l)
if i == 0 {
labels = l
} else if !slicesEqual(labels, l) {
return nil, fmt.Errorf("incompatible recipients")
}
for _, s := range stanzas {
hdr.Recipients = append(hdr.Recipients, (*format.Stanza)(s))
}
@@ -156,6 +169,26 @@ func Encrypt(dst io.Writer, recipients ...Recipient) (io.WriteCloser, error) {
return stream.NewWriter(streamKey(fileKey, nonce), dst)
}
func wrapWithLabels(r Recipient, fileKey []byte) (s []*Stanza, labels []string, err error) {
if r, ok := r.(RecipientWithLabels); ok {
return r.WrapWithLabels(fileKey)
}
s, err = r.Wrap(fileKey)
return
}
func slicesEqual(s1, s2 []string) bool {
if len(s1) != len(s2) {
return false
}
for i := range s1 {
if s1[i] != s2[i] {
return false
}
}
return true
}
// NoIdentityMatchError is returned by Decrypt when none of the supplied
// identities match the encrypted file.
type NoIdentityMatchError struct {
@@ -179,7 +212,7 @@ func Decrypt(src io.Reader, identities ...Identity) (io.Reader, error) {
hdr, payload, err := format.Parse(src)
if err != nil {
return nil, fmt.Errorf("failed to read header: %v", err)
return nil, fmt.Errorf("failed to read header: %w", err)
}
stanzas := make([]*Stanza, 0, len(hdr.Recipients))
@@ -212,7 +245,7 @@ func Decrypt(src io.Reader, identities ...Identity) (io.Reader, error) {
nonce := make([]byte, streamNonceSize)
if _, err := io.ReadFull(payload, nonce); err != nil {
return nil, fmt.Errorf("failed to read nonce: %v", err)
return nil, fmt.Errorf("failed to read nonce: %w", err)
}
return stream.NewReader(streamKey(fileKey, nonce), payload)
+65 -1
View File
@@ -75,7 +75,7 @@ func ExampleDecrypt() {
}
func ExampleParseIdentities() {
keyFile, err := os.Open("testdata/keys.txt")
keyFile, err := os.Open("testdata/example_keys.txt")
if err != nil {
log.Fatalf("Failed to open private keys file: %v", err)
}
@@ -220,3 +220,67 @@ AGE-SECRET-KEY--1D6K0SGAX3NU66R4GYFZY0UQWCLM3UUSF3CXLW4KXZM342WQSJ82QKU59Q`},
})
}
}
type testRecipient struct {
labels []string
}
func (testRecipient) Wrap(fileKey []byte) ([]*age.Stanza, error) {
panic("expected WrapWithLabels instead")
}
func (t testRecipient) WrapWithLabels(fileKey []byte) (s []*age.Stanza, labels []string, err error) {
return []*age.Stanza{{Type: "test"}}, t.labels, nil
}
func TestLabels(t *testing.T) {
scrypt, err := age.NewScryptRecipient("xxx")
if err != nil {
t.Fatal(err)
}
i, err := age.GenerateX25519Identity()
if err != nil {
t.Fatal(err)
}
x25519 := i.Recipient()
pqc := testRecipient{[]string{"postquantum"}}
pqcAndFoo := testRecipient{[]string{"postquantum", "foo"}}
fooAndPQC := testRecipient{[]string{"foo", "postquantum"}}
if _, err := age.Encrypt(io.Discard, scrypt, scrypt); err == nil {
t.Error("expected two scrypt recipients to fail")
}
if _, err := age.Encrypt(io.Discard, scrypt, x25519); err == nil {
t.Error("expected x25519 mixed with scrypt to fail")
}
if _, err := age.Encrypt(io.Discard, x25519, scrypt); err == nil {
t.Error("expected x25519 mixed with scrypt to fail")
}
if _, err := age.Encrypt(io.Discard, pqc, x25519); err == nil {
t.Error("expected x25519 mixed with pqc to fail")
}
if _, err := age.Encrypt(io.Discard, x25519, pqc); err == nil {
t.Error("expected x25519 mixed with pqc to fail")
}
if _, err := age.Encrypt(io.Discard, pqc, pqc); err != nil {
t.Errorf("expected two pqc to work, got %v", err)
}
if _, err := age.Encrypt(io.Discard, pqc); err != nil {
t.Errorf("expected one pqc to work, got %v", err)
}
if _, err := age.Encrypt(io.Discard, pqcAndFoo, pqc); err == nil {
t.Error("expected pqc+foo mixed with pqc to fail")
}
if _, err := age.Encrypt(io.Discard, pqc, pqcAndFoo); err == nil {
t.Error("expected pqc+foo mixed with pqc to fail")
}
if _, err := age.Encrypt(io.Discard, pqc, pqc, pqcAndFoo); err == nil {
t.Error("expected pqc+foo mixed with pqc to fail")
}
if _, err := age.Encrypt(io.Discard, pqcAndFoo, pqcAndFoo); err != nil {
t.Errorf("expected two pqc+foo to work, got %v", err)
}
if _, err := age.Encrypt(io.Discard, pqcAndFoo, fooAndPQC); err != nil {
t.Errorf("expected pqc+foo mixed with foo+pqc to work, got %v", err)
}
}
+3
View File
@@ -274,6 +274,9 @@ func ParseIdentity(pemBytes []byte) (age.Identity, error) {
switch k := k.(type) {
case *ed25519.PrivateKey:
return NewEd25519Identity(*k)
// ParseRawPrivateKey returns inconsistent types. See Issue 429.
case ed25519.PrivateKey:
return NewEd25519Identity(k)
case *rsa.PrivateKey:
return NewRSAIdentity(k)
}
+14 -8
View File
@@ -5,6 +5,7 @@
package agessh
import (
"crypto"
"crypto/ed25519"
"crypto/rsa"
"fmt"
@@ -105,19 +106,20 @@ func (i *EncryptedSSHIdentity) Unwrap(stanzas []*age.Stanza) (fileKey []byte, er
return nil, fmt.Errorf("failed to decrypt SSH key file: %v", err)
}
var pubKey interface {
Equal(x crypto.PublicKey) bool
}
switch k := k.(type) {
case *ed25519.PrivateKey:
i.decrypted, err = NewEd25519Identity(*k)
// TODO: here and below, better check that the two public keys match,
// rather than just the type.
if i.pubKey.Type() != ssh.KeyAlgoED25519 {
return nil, fmt.Errorf("mismatched private (%s) and public (%s) SSH key types", ssh.KeyAlgoED25519, i.pubKey.Type())
}
pubKey = k.Public().(ed25519.PublicKey)
// ParseRawPrivateKey returns inconsistent types. See Issue 429.
case ed25519.PrivateKey:
i.decrypted, err = NewEd25519Identity(k)
pubKey = k.Public().(ed25519.PublicKey)
case *rsa.PrivateKey:
i.decrypted, err = NewRSAIdentity(k)
if i.pubKey.Type() != ssh.KeyAlgoRSA {
return nil, fmt.Errorf("mismatched private (%s) and public (%s) SSH key types", ssh.KeyAlgoRSA, i.pubKey.Type())
}
pubKey = &k.PublicKey
default:
return nil, fmt.Errorf("unexpected SSH key type: %T", k)
}
@@ -125,5 +127,9 @@ func (i *EncryptedSSHIdentity) Unwrap(stanzas []*age.Stanza) (fileKey []byte, er
return nil, fmt.Errorf("invalid SSH key: %v", err)
}
if exp := i.pubKey.(ssh.CryptoPublicKey).CryptoPublicKey(); !pubKey.Equal(exp) {
return nil, fmt.Errorf("mismatched private and public SSH key")
}
return i.decrypted.Unwrap(stanzas)
}
+53 -12
View File
@@ -14,6 +14,7 @@ import (
"bytes"
"encoding/base64"
"errors"
"fmt"
"io"
"filippo.io/age/internal/format"
@@ -88,22 +89,47 @@ func (r *armoredReader) Read(p []byte) (int, error) {
getLine := func() ([]byte, error) {
line, err := r.r.ReadBytes('\n')
if err != nil && len(line) == 0 {
if err == io.EOF {
err = errors.New("invalid armor: unexpected EOF")
}
if err == io.EOF && len(line) == 0 {
return nil, io.ErrUnexpectedEOF
} else if err != nil && err != io.EOF {
return nil, err
}
return bytes.TrimSpace(line), nil
line = bytes.TrimSuffix(line, []byte("\n"))
line = bytes.TrimSuffix(line, []byte("\r"))
return line, nil
}
if !r.started {
const maxWhitespace = 1024
drainTrailing := func() error {
buf, err := io.ReadAll(io.LimitReader(r.r, maxWhitespace))
if err != nil {
return err
}
if len(bytes.TrimSpace(buf)) != 0 {
return errors.New("trailing data after armored file")
}
if len(buf) == maxWhitespace {
return errors.New("too much trailing whitespace")
}
return io.EOF
}
var removedWhitespace int
for !r.started {
line, err := getLine()
if err != nil {
return 0, r.setErr(err)
}
// Ignore leading whitespace.
if len(bytes.TrimSpace(line)) == 0 {
removedWhitespace += len(line) + 1
if removedWhitespace > maxWhitespace {
return 0, r.setErr(errors.New("too much leading whitespace"))
}
continue
}
if string(line) != Header {
return 0, r.setErr(errors.New("invalid armor first line: " + string(line)))
return 0, r.setErr(fmt.Errorf("invalid first line: %q", line))
}
r.started = true
}
@@ -112,15 +138,15 @@ func (r *armoredReader) Read(p []byte) (int, error) {
return 0, r.setErr(err)
}
if string(line) == Footer {
return 0, r.setErr(io.EOF)
return 0, r.setErr(drainTrailing())
}
if len(line) > format.ColumnsPerLine {
return 0, r.setErr(errors.New("invalid armor: column limit exceeded"))
return 0, r.setErr(errors.New("column limit exceeded"))
}
r.unread = r.buf[:]
n, err := base64.StdEncoding.Strict().Decode(r.unread, line)
if err != nil {
return 0, r.setErr(errors.New("invalid armor: " + err.Error()))
return 0, r.setErr(err)
}
r.unread = r.unread[:n]
@@ -130,9 +156,9 @@ func (r *armoredReader) Read(p []byte) (int, error) {
return 0, r.setErr(err)
}
if string(line) != Footer {
return 0, r.setErr(errors.New("invalid armor closing line: " + string(line)))
return 0, r.setErr(fmt.Errorf("invalid closing line: %q", line))
}
r.err = io.EOF
r.setErr(drainTrailing())
}
nn := copy(p, r.unread)
@@ -140,7 +166,22 @@ func (r *armoredReader) Read(p []byte) (int, error) {
return nn, nil
}
type Error struct {
err error
}
func (e *Error) Error() string {
return "invalid armor: " + e.err.Error()
}
func (e *Error) Unwrap() error {
return e.err
}
func (r *armoredReader) setErr(err error) error {
if err != io.EOF {
err = &Error{err}
}
r.err = err
return err
}
+52
View File
@@ -2,6 +2,9 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build go1.18
// +build go1.18
package armor_test
import (
@@ -11,6 +14,8 @@ import (
"fmt"
"io"
"log"
"os"
"path/filepath"
"strings"
"testing"
@@ -128,3 +133,50 @@ func testArmor(t *testing.T, size int) {
t.Error("decoded value doesn't match")
}
}
func FuzzMalleability(f *testing.F) {
tests, err := filepath.Glob("../testdata/testkit/*")
if err != nil {
f.Fatal(err)
}
for _, test := range tests {
contents, err := os.ReadFile(test)
if err != nil {
f.Fatal(err)
}
header, contents, ok := bytes.Cut(contents, []byte("\n\n"))
if !ok {
f.Fatal("testkit file without header")
}
if bytes.Contains(header, []byte("armored: yes")) {
f.Add(contents)
}
}
f.Fuzz(func(t *testing.T, data []byte) {
r := armor.NewReader(bytes.NewReader(data))
content, err := io.ReadAll(r)
if err != nil {
if _, ok := err.(*armor.Error); !ok {
t.Errorf("error type is %T: %v", err, err)
}
t.Skip()
}
buf := &bytes.Buffer{}
w := armor.NewWriter(buf)
if _, err := w.Write(content); err != nil {
t.Fatal(err)
}
if err := w.Close(); err != nil {
t.Fatal(err)
}
if !bytes.Equal(normalize(buf.Bytes()), normalize(data)) {
t.Error("re-encoded output different from input")
}
})
}
func normalize(f []byte) []byte {
f = bytes.TrimSpace(f)
f = bytes.Replace(f, []byte("\r\n"), []byte("\n"), -1)
return f
}
+20 -8
View File
@@ -18,7 +18,7 @@ import (
"filippo.io/age"
"filippo.io/age/agessh"
"filippo.io/age/armor"
"filippo.io/age/internal/plugin"
"filippo.io/age/plugin"
"golang.org/x/term"
)
@@ -103,7 +103,7 @@ func main() {
if len(os.Args) == 1 {
flag.Usage()
os.Exit(1)
exit(1)
}
var (
@@ -234,6 +234,16 @@ func main() {
in = f
} else {
stdinInUse = true
if decryptFlag && term.IsTerminal(int(os.Stdin.Fd())) {
// If the input comes from a TTY, assume it's armored, and buffer up
// to the END line (or EOF/EOT) so that a password prompt or the
// output don't get in the way of typing the input. See Issue 364.
buf, err := bufferTerminalInput(in)
if err != nil {
errorf("failed to buffer terminal input: %v", err)
}
in = buf
}
}
if name := outFlag; name != "" && name != "-" {
f := newLazyOpener(name)
@@ -288,12 +298,10 @@ func passphrasePromptForEncryption() (string, error) {
words = append(words, randomWord())
}
p = strings.Join(words, "-")
// It's somewhat unfortunate that the prompt comes through the terminal,
// while the autogenerated passphrase is printed to stderr. However,
// thinking about the terminal as a pinentry UI, it's better for the
// passphrase to stick around and be copy-pastable, than to show up in
// ephemeral UI.
printf("using autogenerated passphrase %q", p)
err := printfToTerminal("using autogenerated passphrase %q", p)
if err != nil {
return "", fmt.Errorf("could not print passphrase: %v", err)
}
} else {
confirm, err := readSecret("Confirm passphrase:")
if err != nil {
@@ -360,9 +368,12 @@ func encryptPass(in io.Reader, out io.Writer, armor bool) {
if err != nil {
errorf("%v", err)
}
testOnlyConfigureScryptIdentity(r)
encrypt([]age.Recipient{r}, in, out, armor)
}
var testOnlyConfigureScryptIdentity = func(*age.ScryptRecipient) {}
func encrypt(recipients []age.Recipient, in io.Reader, out io.Writer, withArmor bool) {
if withArmor {
a := armor.NewWriter(out)
@@ -454,6 +465,7 @@ func decrypt(identities []age.Identity, in io.Reader, out io.Writer) {
if err != nil {
errorf("%v", err)
}
out.Write(nil) // trigger the lazyOpener even if r is empty
if _, err := io.Copy(out, r); err != nil {
errorf("%v", err)
}
+66 -80
View File
@@ -5,93 +5,79 @@
package main
import (
"errors"
"io"
"bufio"
"os"
"path/filepath"
"strings"
"testing"
"filippo.io/age"
"github.com/rogpeppe/go-internal/testscript"
)
func TestVectors(t *testing.T) {
var defaultIDs []age.Identity
password, err := os.ReadFile("testdata/default_password.txt")
if err != nil {
t.Fatal(err)
}
p := strings.TrimSpace(string(password))
i, err := age.NewScryptIdentity(p)
if err != nil {
t.Fatal(err)
}
defaultIDs = append(defaultIDs, i)
ids, err := parseIdentitiesFile("testdata/default_key.txt")
if err != nil {
t.Fatal(err)
}
defaultIDs = append(defaultIDs, ids...)
files, _ := filepath.Glob("testdata/*.age")
for _, f := range files {
_, name := filepath.Split(f)
name = strings.TrimSuffix(name, ".age")
expectPass := strings.HasPrefix(name, "good_")
expectFailure := strings.HasPrefix(name, "fail_")
expectNoMatch := strings.HasPrefix(name, "nomatch_")
t.Run(name, func(t *testing.T) {
identities := defaultIDs
ids, err := parseIdentitiesFile("testdata/" + name + "_key.txt")
if err == nil {
identities = ids
func TestMain(m *testing.M) {
os.Exit(testscript.RunMain(m, map[string]func() int{
"age": func() (exitCode int) {
testOnlyPanicInsteadOfExit = true
defer func() {
if testOnlyDidExit {
exitCode = recover().(int)
}
}()
testOnlyConfigureScryptIdentity = func(r *age.ScryptRecipient) {
r.SetWorkFactor(10)
}
password, err := os.ReadFile("testdata/" + name + "_password.txt")
if err == nil {
p := strings.TrimSpace(string(password))
i, err := age.NewScryptIdentity(p)
if err != nil {
t.Fatal(err)
}
identities = []age.Identity{i}
testOnlyFixedRandomWord = "four"
main()
return 0
},
"age-plugin-test": func() (exitCode int) {
// TODO: use plugin server package once it's available.
switch os.Args[1] {
case "--age-plugin=recipient-v1":
scanner := bufio.NewScanner(os.Stdin)
scanner.Scan() // add-recipient
scanner.Scan() // body
scanner.Scan() // grease
scanner.Scan() // body
scanner.Scan() // wrap-file-key
scanner.Scan() // body
fileKey := scanner.Text()
scanner.Scan() // extension-labels
scanner.Scan() // body
scanner.Scan() // done
scanner.Scan() // body
os.Stdout.WriteString("-> recipient-stanza 0 test\n")
os.Stdout.WriteString(fileKey + "\n")
scanner.Scan() // ok
scanner.Scan() // body
os.Stdout.WriteString("-> done\n\n")
return 0
case "--age-plugin=identity-v1":
scanner := bufio.NewScanner(os.Stdin)
scanner.Scan() // add-identity
scanner.Scan() // body
scanner.Scan() // grease
scanner.Scan() // body
scanner.Scan() // recipient-stanza
scanner.Scan() // body
fileKey := scanner.Text()
scanner.Scan() // done
scanner.Scan() // body
os.Stdout.WriteString("-> file-key 0\n")
os.Stdout.WriteString(fileKey + "\n")
scanner.Scan() // ok
scanner.Scan() // body
os.Stdout.WriteString("-> done\n\n")
return 0
default:
return 1
}
},
}))
}
in, err := os.Open("testdata/" + name + ".age")
if err != nil {
t.Fatal(err)
}
r, err := age.Decrypt(in, identities...)
if expectFailure {
if err == nil {
_, err = io.ReadAll(r)
}
if err == nil {
t.Fatal("expected Decrypt or Read failure")
}
if e := new(age.NoIdentityMatchError); errors.As(err, &e) {
t.Errorf("got ErrIncorrectIdentity, expected more specific error")
}
} else if expectNoMatch {
if err == nil {
t.Fatal("expected Decrypt failure")
}
if e := new(age.NoIdentityMatchError); !errors.As(err, &e) {
t.Errorf("expected ErrIncorrectIdentity, got %v", err)
}
} else if expectPass {
if err != nil {
t.Fatal(err)
}
out, err := io.ReadAll(r)
if err != nil {
t.Fatal(err)
}
t.Logf("%s", out)
} else {
t.Fatal("invalid test vector: missing prefix")
}
})
}
func TestScript(t *testing.T) {
testscript.Run(t, testscript.Params{
Dir: "testdata",
// TODO: enable AGEDEBUG=plugin without breaking stderr checks.
})
}
+1 -1
View File
@@ -15,7 +15,7 @@ import (
"filippo.io/age"
"filippo.io/age/agessh"
"filippo.io/age/armor"
"filippo.io/age/internal/plugin"
"filippo.io/age/plugin"
"golang.org/x/crypto/cryptobyte"
"golang.org/x/crypto/ssh"
)
-6
View File
@@ -1,6 +0,0 @@
# created: 2021-02-02T13:09:43+01:00
# public key: age1xmwwc06ly3ee5rytxm9mflaz2u56jjj36s0mypdrwsvlul66mv4q47ryef
AGE-SECRET-KEY-1EGTZVFFV20835NWYV6270LXYVK2VKNX2MMDKWYKLMGR48UAWX40Q2P2LM0
# TODO: regenerate empty_recipient_body.age
AGE-SECRET-KEY-1TRYTV7PQS5XPUYSTAQZCD7DQCWC7Q77YJD7UVFJRMW4J82Q6930QS70MRX
-1
View File
@@ -1 +0,0 @@
now-major-idea-author-clerk-bronze-all-soul-uncover-glad
+32
View File
@@ -0,0 +1,32 @@
# encrypt and decrypt a file with -R
age -R key.pem.pub -o test.age input
age -d -i key.pem test.age
cmp stdout input
! stderr .
# encrypt and decrypt a file with -i
age -e -i key.pem -o test.age input
age -d -i key.pem test.age
cmp stdout input
! stderr .
# encrypt and decrypt a file with the wrong key
age -R otherkey.pem.pub -o test.age input
! age -d -i key.pem test.age
stderr 'no identity matched any of the recipients'
! stdout .
-- input --
test
-- key.pem --
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACB/aTuac9tiWRGrKEtixFlryYlGCPTOpdbmXN9RRmDF2gAAAKDgV/GC4Ffx
ggAAAAtzc2gtZWQyNTUxOQAAACB/aTuac9tiWRGrKEtixFlryYlGCPTOpdbmXN9RRmDF2g
AAAECvFoQXQzXgJLQ+Gz4PfEcfyZwC2gUjOiWTD//mTPyD8H9pO5pz22JZEasoS2LEWWvJ
iUYI9M6l1uZc31FGYMXaAAAAG2ZpbGlwcG9AQmlzdHJvbWF0aC1NMS5sb2NhbAEC
-----END OPENSSH PRIVATE KEY-----
-- key.pem.pub --
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH9pO5pz22JZEasoS2LEWWvJiUYI9M6l1uZc31FGYMXa
-- otherkey.pem.pub --
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJFlMdZUMrWjJ3hh60MLALXSqUdAjBo/qEMJzvpekpoM
+125
View File
@@ -0,0 +1,125 @@
# TODO: age-encrypted private keys, multiple identities, -i ordering, -e -i,
# age file password prompt during encryption
[!linux] [!darwin] skip # no pty support
# use an encrypted OpenSSH private key without .pub file
age -R key_ed25519.pub -o ed25519.age input
rm key_ed25519.pub
ttyin terminal
age -d -i key_ed25519 ed25519.age
cmp stdout input
! stderr .
# -e -i with an encrypted OpenSSH private key
age -e -i key_ed25519 -o ed25519.age input
ttyin terminal
age -d -i key_ed25519 ed25519.age
cmp stdout input
# a file encrypted to the wrong key does not ask for the password
age -R key_ed25519_other.pub -o ed25519_other.age input
! age -d -i key_ed25519 ed25519_other.age
stderr 'no identity matched any of the recipients'
# use an encrypted legacy PEM private key with a .pub file
age -R key_rsa_legacy.pub -o rsa_legacy.age input
ttyin terminal
age -d -i key_rsa_legacy rsa_legacy.age
cmp stdout input
! stderr .
age -R key_rsa_other.pub -o rsa_other.age input
! age -d -i key_rsa_legacy rsa_other.age
stderr 'no identity matched any of the recipients'
# -e -i with an encrypted legacy PEM private key
age -e -i key_rsa_legacy -o rsa_legacy.age input
ttyin terminal
age -d -i key_rsa_legacy rsa_legacy.age
cmp stdout input
# legacy PEM private key without a .pub file causes an error
rm key_rsa_legacy.pub
! age -d -i key_rsa_legacy rsa_legacy.age
stderr 'key_rsa_legacy.pub'
# mismatched .pub file causes an error
cp key_rsa_legacy key_rsa_other
ttyin terminal
! age -d -i key_rsa_other rsa_other.age
stderr 'mismatched private and public SSH key'
# buffer armored ciphertext before prompting if stdin is the terminal
ttyin terminal
age -e -i key_ed25519 -a -o test.age input
exec cat test.age terminal # concatenated ciphertext + password
ttyin -stdin stdout
age -d -i key_ed25519
ttyout 'Enter passphrase'
! stderr .
cmp stdout input
-- input --
test
-- terminal --
password
-- key_ed25519 --
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABCuvb97i7
U6Dz4+4SaF3kK1AAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIKaVctg4/hmFbfof
Tv+yrC2IweO/Dd2AVDijFpaMO9fmAAAAoMO7yEnisRmzFdiExNt3XTYuLdP9m3jgOCroiF
TtBhh1lAB2qggzWExMRP3Ak8+AloXEcWiACwBYnqwxhQMh0RDCDKC/H/4SXO+ds4HFWil+
4bGF9wYZFU7IEjIK91CPGJ6YoWPn9dSdEjjbuCJtOMwHsysGyw5n/qSFPmSAPmA4YL2OzM
WFOJ5gB5o1LKZkDTcdt7kPziIoVd5QkqpnYsE=
-----END OPENSSH PRIVATE KEY-----
-- key_ed25519.pub --
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKaVctg4/hmFbfofTv+yrC2IweO/Dd2AVDijFpaMO9fm
-- key_ed25519_other.pub --
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINbTd+xfSBYKR/1Hp7FsoxwQAdIOk1Khye6ALBj7e1CV
-- key_rsa_legacy --
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-128-CBC,8045E7CF19D7794F4ADF5AC63179D985
OESHhWCho337W1Ajg+iMbsZx/FPtHM3YPHu/d1U51ERIUh0wVof2SK0ooENokr6g
O3fcv9Xga+Na4Ez+gsFRsIZOdqrJq+QBH0CAKi+Mz4KsU7teAobUBJgRB31Wt7eI
39KGZeaBJLMQ0FzQkDx5MCOg98iu9rt+Pg1bH8X88wV4vOv+tG4nmqgdpDmouo1Q
uW1TJxrdPhkINjaPZZ7gvjS8wuG9+qwQY76I0hGun9secf4VZDysqUnUp8UHYovR
dbvKCbglQy18mGL4kREJ/hH/9/maefS+pTMb2UX0onp9j7l3yNSvL4A4xW85ii6x
liVMnZvLvbfPtI7jjZtC8CjshRkZke4fSZF2nZP7zK2qVcqDFCtemaks+0i2ksel
D8clUKhBmq23VNAt+iy1stwHBporuaE6kEVJail5WPpgdfQjifpaMbTsZgOK+vGL
GKi8vSJWfMU3lTf/N++ks2FWxdq0TgQirsKsQ5mWobfxc1XehvvdJj8hUtArrP32
d4ge5DXPpmtkCzrc1+wt8Py/ANl9jV6c+4fCbpQ2snyzdFEhFtXHCEpMguN9MhKI
gaZIfAxvYcQr8Gwew/IB02Phda9tvDiedHvyHGJmSy/87fR6ECh47VDFL/UYu4jG
0hRtAZMMddGNfoosnO6OKBd09cgvXKCsUrbpAI7dF5TP5ItDkVb08hW446jBdgS9
7QqB0rPmlAjsJi7fsrDw7Nq9pOdqqCEwUMc9Lztnv66aX1d/Y2vQm9mrsDbyZKqn
g621rg7E4UHf7EGiDblfS234+TsNvwZ6sEbivU+3zqglPiOF71m6D0cKgaUZPOua
GNdyQz5e73hYa+NJ76IZ+IqkoJAFXBkd1nWcN6DUBYiKvqd4qO9xD+JvNtiFlQ9d
pyO9t4FTGvySh8CKyEUEdtj+2ftCIuZaUD2L5YJU1tlQV0EH42InOmkmphbHkW5v
lNAoZAny1Z0P6O0gn7xtVrgd7paVQfDCJtkvsm5zR6Yei5FUgY/9NPaRotzuZVAY
EfQC7JPdSdb5yusnXh7B9jGkgxhMIb6EPFFjIZ4iaV1RVgINSisGMSFzlqOz702b
Cawsr9nD438cjzMNYEmrihZZBjHon6hHrLmM9Aj2xgprsoNLP1jJQ6WpZDlrYsj0
XS0tSJmh0pM4Ey6j1VWNoaOxVseYLW7J9wGVfH/HJAc2k6Wg46P2e8lMT6Sj4YsT
EguDhUjXrgePC53ohcSF+I6x35Q1D6ttMnc3ODzmIcCisxAvWdAqi1yRlnBotRwg
S2vq3HU0yJFG8pJqw4vU9A9DlaMMT+ejEH+9xVwAWM+7n2lJcgthtWuShZCE6BB+
jVobSlTMArzQj4klTSbew1m9Waa6kKDezsAY66mryVNofCCeYDOBRecCm5JyMnWf
WBVnNx+kZ/YyvYeBcSh34u8rkjqGpzfM/oPE7GwIoZvbAirjLohL7u8oq2bfAYG0
/xIPwPJw1O3o5PHeu84bVIRqcKzGeaVL+5aUiZP9uNGUpqJWA5q2Sa5BOXV46yqO
DIS8q7uPCSbt5mPXPDGJ1CupCdA1stUf2kb0cDJ+LpUbPND9SebBlxSuR1D/YGqv
wlzfN5Usv/h/XNl98bYtpY8/skKPecyx3wG3VtwWH/5XVhvHz4TENjlKv/L2pbUC
Dv83WcL1N/i+jerYxDRmGe3NQOvyW4JaNzzjgb74T7rE1/3lf6qkmUHjxfo4VZAF
L/q2782OUs5Qt4/pYAIISzLdBw6XtTjZHirqa6YNrFvGucB3NG49AC0b1Z0acfrS
iimC2TvZpwunlLbyz2SQQL2c1zQ3U/Yfh2F1Zt8o6kK3RgKSSx57rK6nV7hXMGGp
C4HV3nLetZg8HexicqeRANLXuUDbCSpN8K4nW5G2g/yKPfsQHBV/RWEDfhndykja
+SmoY5IB+2zEbCC3MWiP9ZdIcCYOsq8wDZESMMW40DlVICjrf6UOqQ+ogci20qLS
CmpgmOPAaBZJG/sBU79eHUSjPCK6yDpSyc30oVn8FnoBTmOpt7R++Ub8RJxReXBt
+6o0NXYCJNaeVnk1bE4iavkJrXJCZvu44VBLS0WUs9W8TD4Iq8kNHsfQsfOuBXnQ
ncgoIe9HppnMGNoSzjYBNL/rprlbaOE55TkPqiQsiskRcaoeY53aTxoIykHmoj8G
wJo/00IR+NYir7tr03Vriw+uywPPGucVJGWTUGsNbHlS5j941IltflIf6FitElNr
JxVuJLgYiP3JhmWpdqA/uidYJMbIjunpn/8rVLrAil04SCSfUmaCdl7dkQ9x+3Mf
Erm699vIBQwvv0i+mcwKEvqSrhhNQ2F7vrb7NL8I2wUEPgQbv1PxSV6X0aYcxYVI
-----END RSA PRIVATE KEY-----
-- key_rsa_legacy.pub --
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCky7Clp8I3LVoqZWtat+QR6KmM0evFilmFhwenINIBbb8eS3ftDSkQy2YRrlAvO3h4EZffOIxANGL/yKVlRCIzvjsphi+tTHscZsQhwMnLEmxEayTq20hZKcwNA8TQdh2TW/w0KZmNZcxlTn4IK8W16komHcoH/qrRiXq8z3ROcfnv3Q4Hll9MUCwBkfy2DdBpWUMidQ1dAK4i3vXdseF74hJ0jFbPtS5mlpOsJZa0sdH1dnEl5M8wZS3PxyzM6JMkgzG7INp4sO/xGIisjl/QuSh2Fu93/EogdGXxIZChniUfzBx1DaHlerPPNSMP+uLbaOIAQrIPozhfdUdsCFDMoB7/PA6g1WVYZWAqjBZZW/GMOzPhih57NIFBSyMTzMi1KS6OBvYJvPf4IcvOa3May9ylLG/wZVhrHlQPbSsbRrraVtJ1P4gGQJ5U4d2AD2q+XtMb5f2i/holMXTVQl7Fa7RYi1TblDuW5OZCvmIawePBXAYbPg0OVFs3vAVEuAM=
-- key_rsa_other.pub --
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDQiCWw2W++gX4wcwpDo6QIouwQ9PPwCVe7QPICzxztG27mzeKRM4xT2LURGSaQqg7OYIUTGrLqNsaLZW+FHHQlRAVv1LEbdEFa5JermBMJ5j/HxamE/7oV60gMRlgKW+4IZhVMPgRZaaXU0YPb9oACdMNM8kPkc5JaOJ8iO6B1RViybjLD+tsEEPXLp3Mrj+sJqs+IvNlJKXdeefOjNrGmLHKIFdHiWlZ+aAW+QLfMQiNXoTbGybFUSpNEbmK/1ITiRAly94NoUK9LoriueXR+WJIm9wP4SfHw+hMBz1cywdF2wwKmWWegizV/USEmhyNXUzHZzjbkgE84DrIq+NA7SUmw6C8ClMjdnRnnoIyga99yMIrYMny1KW/bk1NK4u6Tv17E+FFOS3vf2Gcj01/jOmAUIQwL8MjAHhnsZ4XAA5NHa2NRGWm+hw7fx5uX42Gyz8HidFda5Lij1pASBcx4U3qwb62X+IVN50jGIP6kRNmGtMLY1JgaoGDDkw9r6mU=
-5
View File
@@ -1,5 +0,0 @@
age-encryption.org/v1
-> X25519 i6JOY3uvMdBuEybYbTp3ECFsOPEY/A3lJY1l0Qv2NC4
cD7VpfIOchU6ZjAccEjlPCNSOdJvVkxZPSf+7XS1YhY
--- 1111111111111111111111111111111111111111111
�-\�P9��0�hń��Tt�|:٘�#&R�r� ��
-7
View File
@@ -1,7 +0,0 @@
age-encryption.org/v1
-> scrypt z8U9dYMQuK1fFdvtpQYLEQ 10
5SVjw1bbFCZLdI1FR7RqfTd3yWo4KS1ikOjvz60Bpqhrv0W6o6/2oszxZEm1gEUC
--- YXxSwONGPBbV7woMuEFTYuA03qTYUF1k0Y8j/NDEu1o
’Ì
Å!í‡ i�Þ.f¦æš{ŸdËIE]© ñn”$È!b®2
-6
View File
@@ -1,6 +0,0 @@
age-encryption.org/v1
-> X25519 UkSgrxSETNpdkHY8EwiiRivqks2QJLUzsNsVjUTDcmw
8yB9TqsBo4Ypchw07AtemV5TW4sGwyPDPMIfRg8Ve8rbDXt4tCwnnKcMq2K6aoqx
--- vUhLU0U9Dc8YhbKy4SxKuq0iSqqjBWGnHfZG+9+O4v4
タケ��イ€hエW諾餞I暠fッニ�索Q;ミRh皷w
Binary file not shown.
Binary file not shown.
-5
View File
@@ -1,5 +0,0 @@
age-encryption.org/v1
-> scrypt 1Q6WlGmsRulbN7bmUw8A1Q 23
GP2lnzFuk1dgEkcMPmK6KkmuOm5gIWJzLeuwGcRsvAY
--- vXvOsVbDbMc0x5Js1FS6k1ViOJ3H2ZdSUZo9bfvbzmU
=ñÅÙœ£¨v>¹vhKMû'ÕßúÙ½NeçS†Äñ\(_ó
-5
View File
@@ -1,5 +0,0 @@
age-encryption.org/v1
-> ssh-ed25519 cp09gQ Kf5JDNFFDUaOvups2MDfP47PlrpJthnmz0WNMfGj+C8
hQ76lMAsG2pjR8GHTU+XU0giePyzE3prVmAw5MbMxSk
--- CjO8qf3vd83otqHSflgWP5gQoe2Roo9tf/zgWEy9t0U
èÙ®Ò%JðÇnj<bÖ: %UgZ4#™±àNÂ韟
-7
View File
@@ -1,7 +0,0 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACB/aTuac9tiWRGrKEtixFlryYlGCPTOpdbmXN9RRmDF2gAAAKDgV/GC4Ffx
ggAAAAtzc2gtZWQyNTUxOQAAACB/aTuac9tiWRGrKEtixFlryYlGCPTOpdbmXN9RRmDF2g
AAAECvFoQXQzXgJLQ+Gz4PfEcfyZwC2gUjOiWTD//mTPyD8H9pO5pz22JZEasoS2LEWWvJ
iUYI9M6l1uZc31FGYMXaAAAAG2ZpbGlwcG9AQmlzdHJvbWF0aC1NMS5sb2NhbAEC
-----END OPENSSH PRIVATE KEY-----
-1
View File
@@ -1 +0,0 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH9pO5pz22JZEasoS2LEWWvJiUYI9M6l1uZc31FGYMXa
-5
View File
@@ -1,5 +0,0 @@
age-encryption.org/v1
-> X25519 JRosIz2avWchP2qSL6wF6U7uzD6kDuJXDbZvN1MOGmo
KpIQxpkbBDHqp+JsHLiTy2d5RYRwp2qzvUrAe0aDOnk
--- orVjbqbzm8U3S9njAs53o4PFi1wK39fIQQ4gRj3i7IU
„Ïgñ¾� Ô0‡‘µÆN¢'jûöao�¹&æT
-7
View File
@@ -1,7 +0,0 @@
age-encryption.org/v1
-> X25519 alRneDshIh43nwyD5+fhuTD5TReSn88f2us4hzZPyzU
pGduNK5MUhnuzMxW0qbZnC2k7mRzz69bbJpKQrRc7uc
-> A7)h-grease !,_
--- 5bA0uXjBxI6wuI5SseCRgD5/G8LkSVISRe/hnrQMb9s
¦Ã 1‘·¬Žä6_RÙäÚ…›…U<ï1ús®ÿ?`Ö+–’$§HÖW‚v?w8ZW
Binary file not shown.
-13
View File
@@ -1,13 +0,0 @@
age-encryption.org/v1
-> ssh-rsa jw/33g
xv12FD3f2d7snIcuXBznTOWAlgCovW1Dqttk9uljWKy3GtRZ3t8jEWkQEOYkOk0M
EHA6sHWfdPLdlS3DjQYjcaLFvwh3+XVKYNzP9MhLg8P8xvxVkn4aiCKd8ivEisp0
bKGi4g/TJz/JKUg1SGbqDg966to0P5AWrkwAD7OMykQToqo56flrKXgFPleSWVWu
umiwbxFYs7ltbRYvjzdpIj9l30lXkzrADP3RrrvTu/qT0IN3PMi3bOqm0kKz0vkd
p4NpxKmfqQXavU+YZiyQL637V3cbKIAEJ1qmpkd2Tr2oUhfD5IgAoT1nC5tCIzRb
DkPwM4k2FJgVX0KKvW3i0+k5tve4XWg82vq2OCj8+sl3A8cLX3g5zhh53DovUBVm
qDU2HWf++3q9kUy1al0sFb2es4ih+tK74nPjBJZtX0n+4lMngz557+XuYnzZ2OkW
QEq3b7Trdidw7Ak9S14tdXhj8oy7J1jdHsQ8/wehAc1v8MuBb1O7LxVIFxzBEBCA
--- QdCY4BN4vwp5jb+AFsyoHkvKW+EneZsZjPURH2tCF18
«)KVMARsS‘M’ÚëûØ‚Kí>›ÕÕ«`¤ ~T0n·
-1
View File
@@ -1 +0,0 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDULTit0KUehbigbhsQaZo2oXLgMug8nTfnzDhcf5cEzdOJZyF6i5aRQbqbXIOYeTS3Shpp/iE6d60qi93JBBfveAZqr76tK7lVK8fstvAAGgbo9L9Ru5nhWX0HTlZUOodUT2E0rgAeoFzfvcZTaajzPmikrESmuaJLcdJ3crbCIyovkTxc85KbSn+Ky5grntdVR7GXve0HuIgwuSXGNyO0/hEQyhCEco4w53LZDXl3Dlxgb52u0QCsFFiQJmZ3RWnbCz2mNjM9Vo5KRt2pJ4u74lVsfXCvAqG4GsDd5WXCNRNaQ7gUdw1eSnvvcy3hxeEqLNN9tHaFCjEuyPfXfTVd3t0SlXreyoNbV75rYvZ2F6aLxyMMys+5bpnX0tu2LSq4Bvj5xIsBokjgEd3UXyj1eErsQdUJ0gghAMpXOjVbISwPAGMvUsH2unGbjvfaxnK57S7KAXOYznjhBf5ayIleAc+4LVD/KeMtGbpkyXXozC9jwH9cZBbGU4BtElAbzh8=
-5
View File
@@ -1,5 +0,0 @@
age-encryption.org/v1
-> scrypt qEa/WztCd2KJ4mKwNf1Yrw 10
TQZ4GpAaH4aR4oSDWZTgeRT4wRby4jwmtB02dElWmVQ
--- kOiEP6uoMyK9GKIsV77o4oaPuEr2Q0vdcu+1RKC3lLU
hŸo²P¤V w§\5~4ôënE½oÕd>rOš°mÒÛ¨
-6
View File
@@ -1,6 +0,0 @@
age-encryption.org/v1
-> X25519 kx2RzHNfNuts0I131KwMCyYclZzKCGMzPUaMkH9J4z4
9qEzjtIF4NsLFnxv8EEtCwOQiXj5WHl+HWaDKNeAk+4
--- N+7l3M/ofCyzZVlPJ33CTHH8AddF0itK70QV+IIvXXA
³]Ú É+zAIÉúçê¸Ç�éLüü“ªžà
Hˆ%Ñ¥£
-5
View File
@@ -1,5 +0,0 @@
age-encryption.org/v1
-> scrypt X6oOTRAjCR1xid0PlnNMFA 10
hszKAHhyFVpUgt9niYpdYXVhhN+r+oiCLPZukDdQZBQ
--- 7BRJPVjbIC1JntvHrA13PQrnsa3lkwhnNF/Pbo4BPs4
4|)ÆS|Û‹Ò¿D�÷§}’Í2è%�e�=óœ6˜»ÇZ
-5
View File
@@ -1,5 +0,0 @@
age-encryption.org/v1
-> X25519 Rp86RQ3LgUJpQy4X2RMUhURlBP28tCaLQ2ssysJfRhg
83YXad/lj3/wFM4n7vlGIiBSgfhG8lfiP5U7ajjK3HM
--- O2+UpzetsP2+7BPyGQ4C6VMTY6zwp5TiNpVcFy4qdyM
 璇漡�(擔Q:|c蔑鞮蓤�=f�6b坿!
+54
View File
@@ -0,0 +1,54 @@
# https://github.com/FiloSottile/age/issues/57
age -r age1xmwwc06ly3ee5rytxm9mflaz2u56jjj36s0mypdrwsvlul66mv4q47ryef -o test.age input
! age -o test.out -d -i wrong.txt test.age
! exists test.out
! age -o test.out -d test.age
! exists test.out
! age -o test.out -d -i notexist test.age
! exists test.out
! age -o test.out -d -i wrong.txt notexist
! exists test.out
! age -o test.out -r BAD
! exists test.out
! age -o test.out -r age1xmwwc06ly3ee5rytxm9mflaz2u56jjj36s0mypdrwsvlul66mv4q47ryef notexist
! exists test.out
! age -o test.out -p notexist
! exists test.out
# https://github.com/FiloSottile/age/issues/555
age -r age1xmwwc06ly3ee5rytxm9mflaz2u56jjj36s0mypdrwsvlul66mv4q47ryef -o empty.age empty
exists empty.age
age -d -i key.txt empty.age
! stdout .
! stderr .
age -d -i key.txt -o new empty.age
! stderr .
cmp new empty
[!linux] [!darwin] skip # no pty support
# https://github.com/FiloSottile/age/issues/159
ttyin terminal
age -p -a -o test.age input
ttyin terminalwrong
! age -o test.out -d test.age
ttyout 'Enter passphrase'
stderr 'incorrect passphrase'
! exists test.out
-- terminal --
password
password
-- terminalwrong --
wrong
-- input --
age
-- empty --
-- key.txt --
# created: 2021-02-02T13:09:43+01:00
# public key: age1xmwwc06ly3ee5rytxm9mflaz2u56jjj36s0mypdrwsvlul66mv4q47ryef
AGE-SECRET-KEY-1EGTZVFFV20835NWYV6270LXYVK2VKNX2MMDKWYKLMGR48UAWX40Q2P2LM0
-- wrong.txt --
# created: 2024-06-16T12:14:00+02:00
# public key: age10k7vsqmeg3sp8elfyq5ts55feg4huarpcaf9dmljn9umydg3gymsvx4dp9
AGE-SECRET-KEY-1NPX08S4LELW9K68FKU0U05XXEKG6X7GT004TPNYLF86H3M00D3FQ3VQQNN
+13
View File
@@ -0,0 +1,13 @@
# https://github.com/FiloSottile/age/discussions/428
# encrypt and decrypt a file with an Ed25519 key encoded with PKCS#8
age -e -i key.pem -o test.age input
age -d -i key.pem test.age
cmp stdout input
! stderr .
-- input --
test
-- key.pem --
-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEIJT4Wpo+YG11yybKL/bYXQW7ekz4PAsmV/4tfmY1vU7x
-----END PRIVATE KEY-----
+20
View File
@@ -0,0 +1,20 @@
# encrypt and decrypt a file with a test plugin
age -r age1test10qdmzv9q -o test.age input
age -d -i key.txt test.age
cmp stdout input
! stderr .
# very long identity and recipient
age -R long-recipient.txt -o test.age input
age -d -i long-key.txt test.age
cmp stdout input
! stderr .
-- input --
test
-- key.txt --
AGE-PLUGIN-TEST-10Q32NLXM
-- long-recipient.txt --
age1test10pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7qj6rl8p
-- long-key.txt --
AGE-PLUGIN-TEST-10PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7Q5U8SUD
@@ -1,3 +1,23 @@
# encrypt and decrypt a file with -R
age -R key.pem.pub -o test.age input
age -d -i key.pem test.age
cmp stdout input
! stderr .
# encrypt and decrypt a file with -i
age -e -i key.pem -o test.age input
age -d -i key.pem test.age
cmp stdout input
! stderr .
# encrypt and decrypt a file with the wrong key
age -R otherkey.pem.pub -o test.age input
! age -d -i key.pem test.age
stderr 'no identity matched any of the recipients'
-- input --
test
-- key.pem --
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
NhAAAAAwEAAQAAAYEA1C04rdClHoW4oG4bEGmaNqFy4DLoPJ0358w4XH+XBM3TiWcheouW
@@ -36,3 +56,7 @@ EX0mNDXOlKOP2YAIMrDt5CkPdEh6qQG21LCZXTWmwheZ9iN2vOl/fKqUW9lqd/kTe6WsON
hIpZhs2+oz54Riq1ZwzO9NkcYrvZoDKbDopL1r2ibw0mkgCJrxpWi0Yt2Iooh4GXXqP5C9
T8hrZCbrVJkjKd5QAAABtmaWxpcHBvQEJpc3Ryb21hdGgtTTEubG9jYWwBAgMEBQY=
-----END OPENSSH PRIVATE KEY-----
-- key.pem.pub --
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDULTit0KUehbigbhsQaZo2oXLgMug8nTfnzDhcf5cEzdOJZyF6i5aRQbqbXIOYeTS3Shpp/iE6d60qi93JBBfveAZqr76tK7lVK8fstvAAGgbo9L9Ru5nhWX0HTlZUOodUT2E0rgAeoFzfvcZTaajzPmikrESmuaJLcdJ3crbCIyovkTxc85KbSn+Ky5grntdVR7GXve0HuIgwuSXGNyO0/hEQyhCEco4w53LZDXl3Dlxgb52u0QCsFFiQJmZ3RWnbCz2mNjM9Vo5KRt2pJ4u74lVsfXCvAqG4GsDd5WXCNRNaQ7gUdw1eSnvvcy3hxeEqLNN9tHaFCjEuyPfXfTVd3t0SlXreyoNbV75rYvZ2F6aLxyMMys+5bpnX0tu2LSq4Bvj5xIsBokjgEd3UXyj1eErsQdUJ0gghAMpXOjVbISwPAGMvUsH2unGbjvfaxnK57S7KAXOYznjhBf5ayIleAc+4LVD/KeMtGbpkyXXozC9jwH9cZBbGU4BtElAbzh8=
-- otherkey.pem.pub --
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDF0OPu95EY25O5KmYFLIkiZZFKUlfvaRgmfIT6OcZvPRXBzo0MS/lcrYvAc0RsUVbZ1B3Y9oWmKt/IMXTztCXiza70rO1NI7ciayv5svY/wGMoveutddhA64IjrQKs4m+6Qmjs/dYTnfsk1BzmXrdRKUSqH6c4Id7pRLC1ySLu+4og3nTTpBRBpg+uSkc4Ua6ce6A6RX14PPJ+TAXMfZyKNyaubQhgzLB/CfdXxZqWdAnyooiE7fb6CEB5uppnA5BpPdcWAkSixbwxRHbRC+OSCqMOV6+z+NlO/qSOKJcXfCQnJP/qjJTJde0dYhXG4RILOzIkGVieGJJONDXvj61mMj568IhJz0AEf/UMhvEL79iJ6yZW82Go/zcYkDDfd3KRE3pW+6p9Onu3XqOiQABS+9rEVRBnqYsPajiHBIanBeXpWKGbjznakvxhdRifhOWwAsQDfLmGzh+JnV1vOUjyxKtLNv9zi/oeuYCaIyF7F6en8LMbYSz8YONMZygGxMU=
+59
View File
@@ -0,0 +1,59 @@
[!linux] [!darwin] skip # no pty support
# encrypt with a provided passphrase
stdin input
ttyin terminal
age -p -o test.age
ttyout 'Enter passphrase'
! stderr .
! stdout .
# decrypt with a provided passphrase
ttyin terminal
age -d test.age
ttyout 'Enter passphrase'
! stderr .
cmp stdout input
# decrypt with the wrong passphrase
ttyin wrong
! age -d test.age
stderr 'incorrect passphrase'
# encrypt with a generated passphrase
stdin input
ttyin empty
age -p -o test.age
! stderr .
! stdout .
ttyin autogenerated
age -d test.age
cmp stdout input
# fail when -i is present
ttyin terminal
! age -d -i key.txt test.age
stderr 'file is passphrase-encrypted but identities were specified'
# fail when passphrases don't match
ttyin wrong
! age -p -o fail.age
stderr 'passphrases didn''t match'
! exists fail.age
-- terminal --
password
password
-- wrong --
PASSWORD
password
-- input --
test
-- key.txt --
# created: 2021-02-02T13:09:43+01:00
# public key: age1xmwwc06ly3ee5rytxm9mflaz2u56jjj36s0mypdrwsvlul66mv4q47ryef
AGE-SECRET-KEY-1EGTZVFFV20835NWYV6270LXYVK2VKNX2MMDKWYKLMGR48UAWX40Q2P2LM0
-- autogenerated --
four-four-four-four-four-four-four-four-four-four
-- empty --
+56
View File
@@ -0,0 +1,56 @@
[!linux] [!darwin] skip # no pty support
# controlling terminal is used instead of stdin/stderr
ttyin terminal
age -p -o test.age input
! stderr .
# autogenerated passphrase is printed to terminal
ttyin empty
age -p -o test.age input
ttyout 'autogenerated passphrase'
! stderr .
# with no controlling terminal, stdin terminal is used
## TODO: enable once https://golang.org/issue/53601 is fixed
## and Noctty is added to testscript.
# noctty
# ttyin -stdin terminal
# age -p -o test.age input
# ! stderr .
# no terminal causes an error
## TODO: enable once https://golang.org/issue/53601 is fixed
## and Noctty is added to testscript.
# noctty
# ! age -p -o test.age input
# stderr 'standard input is not a terminal'
# prompt for password before plaintext if stdin is the terminal
exec cat terminal input # concatenated password + input
ttyin -stdin stdout
age -p -a -o test.age
ttyout 'Enter passphrase'
! stderr .
# check the file was encrypted correctly
ttyin terminal
age -d test.age
cmp stdout input
# buffer armored ciphertext before prompting if stdin is the terminal
ttyin terminal
age -p -a -o test.age input
exec cat test.age terminal # concatenated ciphertext + password
ttyin -stdin stdout
age -d
ttyout 'Enter passphrase'
! stderr .
cmp stdout input
-- input --
test
-- terminal --
password
password
-- empty --
+20
View File
@@ -0,0 +1,20 @@
# -help
age -p -help
! stdout .
stderr 'Usage:'
# -h
age -p -h
! stdout .
stderr 'Usage:'
# unknown flag
! age -p -this-flag-does-not-exist
! stdout .
stderr 'flag provided but not defined'
stderr 'Usage:'
# no arguments
! age
! stdout .
stderr 'Usage:'
+23
View File
@@ -0,0 +1,23 @@
# encrypt and decrypt a file with -r
age -r age1xmwwc06ly3ee5rytxm9mflaz2u56jjj36s0mypdrwsvlul66mv4q47ryef -o test.age input
age -d -i key.txt test.age
cmp stdout input
! stderr .
# encrypt and decrypt a file with -i
age -e -i key.txt -o test.age input
age -d -i key.txt test.age
cmp stdout input
! stderr .
# encrypt and decrypt a file with the wrong key
age -r age12phkzssndd5axajas2h74vtge62c86xjhd6u9anyanqhzvdg6sps0xthgl -o test.age input
! age -d -i key.txt test.age
stderr 'no identity matched any of the recipients'
-- input --
test
-- key.txt --
# created: 2021-02-02T13:09:43+01:00
# public key: age1xmwwc06ly3ee5rytxm9mflaz2u56jjj36s0mypdrwsvlul66mv4q47ryef
AGE-SECRET-KEY-1EGTZVFFV20835NWYV6270LXYVK2VKNX2MMDKWYKLMGR48UAWX40Q2P2LM0
+53 -14
View File
@@ -14,6 +14,7 @@ package main
// No capitalized initials and no periods at the end.
import (
"bytes"
"errors"
"fmt"
"io"
@@ -21,7 +22,8 @@ import (
"os"
"runtime"
"filippo.io/age/internal/plugin"
"filippo.io/age/armor"
"filippo.io/age/plugin"
"golang.org/x/term"
)
@@ -34,7 +36,8 @@ func printf(format string, v ...interface{}) {
func errorf(format string, v ...interface{}) {
l.Printf("age: error: "+format, v...)
l.Fatalf("age: report unexpected or unhelpful errors at https://filippo.io/age/report")
l.Printf("age: report unexpected or unhelpful errors at https://filippo.io/age/report")
exit(1)
}
func warningf(format string, v ...interface{}) {
@@ -46,7 +49,22 @@ func errorWithHint(error string, hints ...string) {
for _, hint := range hints {
l.Printf("age: hint: %s", hint)
}
l.Fatalf("age: report unexpected or unhelpful errors at https://filippo.io/age/report")
l.Printf("age: report unexpected or unhelpful errors at https://filippo.io/age/report")
exit(1)
}
// If testOnlyPanicInsteadOfExit is true, exit will set testOnlyDidExit and
// panic instead of calling os.Exit. This way, the wrapper in TestMain can
// recover the panic and return the exit code only if it was originated in exit.
var testOnlyPanicInsteadOfExit bool
var testOnlyDidExit bool
func exit(code int) {
if testOnlyPanicInsteadOfExit {
testOnlyDidExit = true
panic(code)
}
os.Exit(code)
}
// clearLine clears the current line on the terminal, or opens a new line if
@@ -72,29 +90,33 @@ func clearLine(out io.Writer) {
// withTerminal does not open a non-terminal stdin, so the caller does not need
// to check stdinInUse.
func withTerminal(f func(in, out *os.File) error) error {
var in, out *os.File
if runtime.GOOS == "windows" {
var err error
in, err = os.OpenFile("CONIN$", os.O_RDWR, 0)
in, err := os.OpenFile("CONIN$", os.O_RDWR, 0)
if err != nil {
return err
}
defer in.Close()
out, err = os.OpenFile("CONOUT$", os.O_WRONLY, 0)
out, err := os.OpenFile("CONOUT$", os.O_WRONLY, 0)
if err != nil {
return err
}
defer out.Close()
return f(in, out)
} else if tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0); err == nil {
defer tty.Close()
in, out = tty, tty
return f(tty, tty)
} else if term.IsTerminal(int(os.Stdin.Fd())) {
return f(os.Stdin, os.Stdin)
} else {
if !term.IsTerminal(int(os.Stdin.Fd())) {
return fmt.Errorf("standard input is not a terminal, and /dev/tty is not available: %v", err)
}
in, out = os.Stdin, os.Stderr
return fmt.Errorf("standard input is not a terminal, and /dev/tty is not available: %v", err)
}
return f(in, out)
}
func printfToTerminal(format string, v ...interface{}) error {
return withTerminal(func(_, out *os.File) error {
_, err := fmt.Fprintf(out, "age: "+format+"\n", v...)
return err
})
}
// readSecret reads a value from the terminal with no echo. The prompt is ephemeral.
@@ -108,7 +130,7 @@ func readSecret(prompt string) (s []byte, err error) {
return
}
// readSecret reads a single character from the terminal with no echo. The
// readCharacter reads a single character from the terminal with no echo. The
// prompt is ephemeral.
func readCharacter(prompt string) (c byte, err error) {
err = withTerminal(func(in, out *os.File) error {
@@ -185,3 +207,20 @@ var pluginTerminalUI = &plugin.ClientUI{
printf("waiting on %s plugin...", name)
},
}
func bufferTerminalInput(in io.Reader) (io.Reader, error) {
buf := &bytes.Buffer{}
if _, err := buf.ReadFrom(ReaderFunc(func(p []byte) (n int, err error) {
if bytes.Contains(buf.Bytes(), []byte(armor.Footer+"\n")) {
return 0, io.EOF
}
return in.Read(p)
})); err != nil {
return nil, err
}
return buf, nil
}
type ReaderFunc func(p []byte) (n int, err error)
func (f ReaderFunc) Read(p []byte) (n int, err error) { return f(p) }
+5
View File
@@ -10,7 +10,12 @@ import (
"strings"
)
var testOnlyFixedRandomWord string
func randomWord() string {
if testOnlyFixedRandomWord != "" {
return testOnlyFixedRandomWord
}
buf := make([]byte, 2)
if _, err := rand.Read(buf); err != nil {
panic(err)
+3 -35
View File
@@ -1,88 +1,56 @@
.\" generated with Ronn/v0.7.3
.\" http://github.com/rtomayko/ronn/tree/0.7.3
.
.TH "AGE\-KEYGEN" "1" "June 2022" "" ""
.
.\" generated with Ronn-NG/v0.9.1
.\" http://github.com/apjanke/ronn-ng/tree/0.9.1
.TH "AGE\-KEYGEN" "1" "April 2023" ""
.SH "NAME"
\fBage\-keygen\fR \- generate age(1) key pairs
.
.SH "SYNOPSIS"
\fBage\-keygen\fR [\fB\-o\fR \fIOUTPUT\fR]
.
.br
\fBage\-keygen\fR \fB\-y\fR [\fB\-o\fR \fIOUTPUT\fR] [\fIINPUT\fR]
.
.br
.
.SH "DESCRIPTION"
\fBage\-keygen\fR generates a new native age(1) key pair, and outputs the identity to standard output or to the \fIOUTPUT\fR file\. The output includes the public key and the current time as comments\.
.
.P
If the output is not going to a terminal, \fBage\-keygen\fR prints the public key to standard error\.
.
.SH "OPTIONS"
.
.TP
\fB\-o\fR, \fB\-\-output\fR=\fIOUTPUT\fR
Write the identity to \fIOUTPUT\fR instead of standard output\.
.
.IP
If \fIOUTPUT\fR already exists, it is not overwritten\.
.
.TP
\fB\-y\fR
Read an identity file from \fIINPUT\fR or from standard input and output the corresponding recipient(s), one per line, with no comments\.
.
.TP
\fB\-\-version\fR
Print the version and exit\.
.
.SH "EXAMPLES"
Generate a new identity:
.
.IP "" 4
.
.nf
$ age\-keygen
# created: 2021\-01\-02T15:30:45+01:00
# public key: age1lvyvwawkr0mcnnnncaghunadrqkmuf9e6507x9y920xxpp866cnql7dp2z
AGE\-SECRET\-KEY\-1N9JEPW6DWJ0ZQUDX63F5A03GX8QUW7PXDE39N8UYF82VZ9PC8UFS3M7XA9
.
.fi
.
.IP "" 0
.
.P
Write a new identity to \fBkey\.txt\fR:
.
.IP "" 4
.
.nf
$ age\-keygen \-o key\.txt
Public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
.
.fi
.
.IP "" 0
.
.P
Convert an identity to a recipient:
.
.IP "" 4
.
.nf
$ age\-keygen \-y key\.txt
age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
.
.fi
.
.IP "" 0
.
.SH "SEE ALSO"
age(1)
.
.SH "AUTHORS"
Filippo Valsorda \fIage@filippo\.io\fR
+19 -14
View File
@@ -1,8 +1,8 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv='content-type' value='text/html;charset=utf8'>
<meta name='generator' value='Ronn/v0.7.3 (http://github.com/rtomayko/ronn/tree/0.7.3)'>
<meta http-equiv='content-type' content='text/html;charset=utf8'>
<meta name='generator' content='Ronn-NG/v0.9.1 (http://github.com/apjanke/ronn-ng/tree/0.9.1)'>
<title>age-keygen(1) - generate age(1) key pairs</title>
<style type='text/css' media='all'>
/* style: man */
@@ -68,15 +68,16 @@
<li class='tr'>age-keygen(1)</li>
</ol>
<h2 id="NAME">NAME</h2>
<h2 id="NAME">NAME</h2>
<p class="man-name">
<code>age-keygen</code> - <span class="man-whatis">generate <a class="man-ref" href="age.1.html">age<span class="s">(1)</span></a> key pairs</span>
</p>
<h2 id="SYNOPSIS">SYNOPSIS</h2>
<p><code>age-keygen</code> [<code>-o</code> <var>OUTPUT</var>]<br />
<code>age-keygen</code> <code>-y</code> [<code>-o</code> <var>OUTPUT</var>] [<var>INPUT</var>]<br /></p>
<p><code>age-keygen</code> [<code>-o</code> <var>OUTPUT</var>]<br>
<code>age-keygen</code> <code>-y</code> [<code>-o</code> <var>OUTPUT</var>] [<var>INPUT</var>]<br></p>
<h2 id="DESCRIPTION">DESCRIPTION</h2>
@@ -90,15 +91,20 @@ standard error.</p>
<h2 id="OPTIONS">OPTIONS</h2>
<dl>
<dt><code>-o</code>, <code>--output</code>=<var>OUTPUT</var></dt><dd><p> Write the identity to <var>OUTPUT</var> instead of standard output.</p>
<dt>
<code>-o</code>, <code>--output</code>=<var>OUTPUT</var>
</dt>
<dd> Write the identity to <var>OUTPUT</var> instead of standard output.
<p> If <var>OUTPUT</var> already exists, it is not overwritten.</p></dd>
<dt class="flush"><code>-y</code></dt><dd><p> Read an identity file from <var>INPUT</var> or from standard input and output the
corresponding recipient(s), one per line, with no comments.</p></dd>
<dt><code>--version</code></dt><dd><p> Print the version and exit.</p></dd>
<p>If <var>OUTPUT</var> already exists, it is not overwritten.</p>
</dd>
<dt><code>-y</code></dt>
<dd> Read an identity file from <var>INPUT</var> or from standard input and output the
corresponding recipient(s), one per line, with no comments.</dd>
<dt><code>--version</code></dt>
<dd> Print the version and exit.</dd>
</dl>
<h2 id="EXAMPLES">EXAMPLES</h2>
<p>Generate a new identity:</p>
@@ -129,10 +135,9 @@ age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
<p>Filippo Valsorda <a href="mailto:age@filippo.io" data-bare-link="true">age@filippo.io</a></p>
<ol class='man-decor man-foot man foot'>
<li class='tl'></li>
<li class='tc'>June 2022</li>
<li class='tc'>April 2023</li>
<li class='tr'>age-keygen(1)</li>
</ol>
+1 -1
View File
@@ -19,7 +19,7 @@ standard error.
* `-o`, `--output`=<OUTPUT>:
Write the identity to <OUTPUT> instead of standard output.
If <OUTPUT> already exists, it is not overwritten.
* `-y`:
+13 -144
View File
@@ -1,281 +1,184 @@
.\" generated with Ronn/v0.7.3
.\" http://github.com/rtomayko/ronn/tree/0.7.3
.
.TH "AGE" "1" "June 2022" "" ""
.
.\" generated with Ronn-NG/v0.9.1
.\" http://github.com/apjanke/ronn-ng/tree/0.9.1
.TH "AGE" "1" "April 2023" ""
.SH "NAME"
\fBage\fR \- simple, modern, and secure file encryption
.
.SH "SYNOPSIS"
\fBage\fR [\fB\-\-encrypt\fR] (\fB\-r\fR \fIRECIPIENT\fR | \fB\-R\fR \fIPATH\fR)\.\.\. [\fB\-\-armor\fR] [\fB\-o\fR \fIOUTPUT\fR] [\fIINPUT\fR]
.
\fBage\fR [\fB\-\-encrypt\fR] (\fB\-r\fR \fIRECIPIENT\fR | \fB\-R\fR \fIPATH\fR)\|\.\|\.\|\. [\fB\-\-armor\fR] [\fB\-o\fR \fIOUTPUT\fR] [\fIINPUT\fR]
.br
\fBage\fR [\fB\-\-encrypt\fR] \fB\-\-passphrase\fR [\fB\-\-armor\fR] [\fB\-o\fR \fIOUTPUT\fR] [\fIINPUT\fR]
.
.br
\fBage\fR \fB\-\-decrypt\fR [\fB\-i\fR \fIPATH\fR | \fB\-j\fR \fIPLUGIN\fR]\.\.\. [\fB\-o\fR \fIOUTPUT\fR] [\fIINPUT\fR]
.
\fBage\fR \fB\-\-decrypt\fR [\fB\-i\fR \fIPATH\fR | \fB\-j\fR \fIPLUGIN\fR]\|\.\|\.\|\. [\fB\-o\fR \fIOUTPUT\fR] [\fIINPUT\fR]
.br
.
.SH "DESCRIPTION"
\fBage\fR encrypts or decrypts \fIINPUT\fR to \fIOUTPUT\fR\. The \fIINPUT\fR argument is optional and defaults to standard input\. Only a single \fIINPUT\fR file may be specified\. If \fB\-o\fR is not specified, \fIOUTPUT\fR defaults to standard output\.
.
.P
If \fB\-p\fR/\fB\-\-passphrase\fR is specified, the file is encrypted with a passphrase requested interactively\. Otherwise, it\'s encrypted to one or more \fIRECIPIENTS\fR specified with \fB\-r\fR/\fB\-\-recipient\fR or \fB\-R\fR/\fB\-\-recipients\-file\fR\. Every recipient can decrypt the file\.
.
If \fB\-p\fR/\fB\-\-passphrase\fR is specified, the file is encrypted with a passphrase requested interactively\. Otherwise, it's encrypted to one or more \fIRECIPIENTS\fR specified with \fB\-r\fR/\fB\-\-recipient\fR or \fB\-R\fR/\fB\-\-recipients\-file\fR\. Every recipient can decrypt the file\.
.P
In \fB\-d\fR/\fB\-\-decrypt\fR mode, passphrase\-encrypted files are detected automatically and the passphrase is requested interactively\. Otherwise, one or more \fIIDENTITIES\fR specified with \fB\-i\fR/\fB\-\-identity\fR are used to decrypt the file\.
.
.P
\fBage\fR encrypted files are binary and not malleable, with around 200 bytes of overhead per recipient, plus 16 bytes every 64KiB of plaintext\.
.
.SH "OPTIONS"
.
.TP
\fB\-o\fR, \fB\-\-output\fR=\fIOUTPUT\fR
Write encrypted or decrypted file to \fIOUTPUT\fR instead of standard output\. If \fIOUTPUT\fR already exists it will be overwritten\.
.
.IP
If encrypting without \fB\-\-armor\fR, \fBage\fR will refuse to output binary to a TTY\. This can be forced by specifying \fB\-\fR as \fIOUTPUT\fR\.
.
.TP
\fB\-\-version\fR
Print the version and exit\.
.
.SS "Encryption options"
.
.TP
\fB\-e\fR, \fB\-\-encrypt\fR
Encrypt \fIINPUT\fR to \fIOUTPUT\fR\. This is the default\.
.
.TP
\fB\-r\fR, \fB\-\-recipient\fR=\fIRECIPIENT\fR
Encrypt to the explicitly specified \fIRECIPIENT\fR\. See the \fIRECIPIENTS AND IDENTITIES\fR section for possible recipient formats\.
.
.IP
This option can be repeated and combined with other recipient flags, and the file can be decrypted by all provided recipients independently\.
.
.TP
\fB\-R\fR, \fB\-\-recipients\-file\fR=\fIPATH\fR
Encrypt to the \fIRECIPIENTS\fR listed in the file at \fIPATH\fR, one per line\. Empty lines and lines starting with \fB#\fR are ignored as comments\.
.
.IP
If \fIPATH\fR is \fB\-\fR, the recipients are read from standard input\. In this case, the \fIINPUT\fR argument must be specified\.
.
.IP
This option can be repeated and combined with other recipient flags, and the file can be decrypted by all provided recipients independently\.
.
.TP
\fB\-p\fR, \fB\-\-passphrase\fR
Encrypt with a passphrase, requested interactively from the terminal\. \fBage\fR will offer to auto\-generate a secure passphrase\.
.
.IP
This option can\'t be used with other recipient flags\.
.
This option can't be used with other recipient flags\.
.TP
\fB\-a\fR, \fB\-\-armor\fR
Encrypt to an ASCII\-only "armored" encoding\.
.
.IP
\fBage\fR armor is a strict version of PEM with type \fBAGE ENCRYPTED FILE\fR, canonical "strict" Base64, no headers, and no support for leading and trailing extra data\.
.
.IP
Decryption transparently detects and decodes ASCII armoring\.
.
.TP
\fB\-i\fR, \fB\-\-identity\fR=\fIPATH\fR
Encrypt to the \fIRECIPIENTS\fR corresponding to the \fIIDENTITIES\fR listed in the file at \fIPATH\fR\. This is equivalent to converting the file at \fIPATH\fR to a recipients file with \fBage\-keygen \-y\fR and then passing that to \fB\-R\fR/\fB\-\-recipients\-file\fR\.
.
.IP
For the format of \fIPATH\fR, see the definition of \fB\-i\fR/\fB\-\-identity\fR in the \fIDecryption options\fR section\.
.
.IP
\fB\-e\fR/\fB\-\-encrypt\fR must be explicitly specified when using \fB\-i\fR/\fB\-\-identity\fR in encryption mode to avoid confusion\.
.
.TP
\fB\-j\fR \fIPLUGIN\fR
Encrypt using the data\-less \fIplugin\fR \fIPLUGIN\fR\.
.
.IP
This is equivalent to using \fB\-i\fR/\fB\-\-identity\fR with a file that contains a single plugin \fBIDENTITY\fR that encodes no plugin\-specific data\.
.
.IP
\fB\-e\fR/\fB\-\-encrypt\fR must be explicitly specified when using \fB\-j\fR in encryption mode to avoid confusion\.
.
.SS "Decryption options"
.
.TP
\fB\-d\fR, \fB\-\-decrypt\fR
Decrypt \fIINPUT\fR to \fIOUTPUT\fR\.
.
.IP
If \fIINPUT\fR is passphrase encrypted, it will be automatically detected and the passphrase will be requested interactively\. Otherwise, the \fIIDENTITIES\fR specified with \fB\-i\fR/\fB\-\-identity\fR are used\.
.
.IP
ASCII armoring is transparently detected and decoded\.
.
.TP
\fB\-i\fR, \fB\-\-identity\fR=\fIPATH\fR
Decrypt using the \fIIDENTITIES\fR at \fIPATH\fR\.
.
.IP
\fIPATH\fR may be one of the following:
.
.IP
a\. A file listing \fIIDENTITIES\fR one per line\. Empty lines and lines starting with "\fB#\fR" are ignored as comments\.
.
.IP
b\. A passphrase encrypted age file, containing \fIIDENTITIES\fR one per line like above\. The passphrase is requested interactively\. Note that passphrase\-protected identity files are not necessary for most use cases, where access to the encrypted identity file implies access to the whole system\.
.
.IP
c\. An SSH private key file, in PKCS#1, PKCS#8, or OpenSSH format\. If the private key is password\-protected, the password is requested interactively only if the SSH identity matches the file\. See the \fISSH keys\fR section for more information, including supported key types\.
.
.IP
d\. "\fB\-\fR", causing one of the options above to be read from standard input\. In this case, the \fIINPUT\fR argument must be specified\.
.
.IP
This option can be repeated\. Identities are tried in the order in which are provided, and the first one matching one of the file\'s recipients is used\. Unused identities are ignored, but it is an error if the \fIINPUT\fR file is passphrase\-encrypted and \fB\-i\fR/\fB\-\-identity\fR is specified\.
.
This option can be repeated\. Identities are tried in the order in which are provided, and the first one matching one of the file's recipients is used\. Unused identities are ignored, but it is an error if the \fIINPUT\fR file is passphrase\-encrypted and \fB\-i\fR/\fB\-\-identity\fR is specified\.
.TP
\fB\-j\fR \fIPLUGIN\fR
Decrypt using the data\-less \fIplugin\fR \fIPLUGIN\fR\.
.
.IP
This is equivalent to using \fB\-i\fR/\fB\-\-identity\fR with a file that contains a single plugin \fBIDENTITY\fR that encodes no plugin\-specific data\.
.
.SH "RECIPIENTS AND IDENTITIES"
\fBRECIPIENTS\fR are public values, like a public key, that a file can be encrypted to\. \fBIDENTITIES\fR are private values, like a private key, that allow decrypting a file encrypted to the corresponding \fBRECIPIENT\fR\.
.
.SS "Native X25519 keys"
Native \fBage\fR key pairs are generated with age\-keygen(1), and provide small encodings and strong encryption based on X25519\. They are the recommended recipient type for most applications\.
.
.P
A \fBRECIPIENT\fR encoding begins with \fBage1\fR and looks like the following:
.
.IP "" 4
.
.nf
age1gde3ncmahlqd9gg50tanl99r960llztrhfapnmx853s4tjum03uqfssgdh
.
.fi
.
.IP "" 0
.
.P
An \fBIDENTITY\fR encoding begins with \fBAGE\-SECRET\-KEY\-1\fR and looks like the following:
.
.IP "" 4
.
.nf
AGE\-SECRET\-KEY\-1KTYK6RVLN5TAPE7VF6FQQSKZ9HWWCDSKUGXXNUQDWZ7XXT5YK5LSF3UTKQ
.
.fi
.
.IP "" 0
.
.P
An encrypted file can\'t be linked to the native recipient it\'s encrypted to without access to the corresponding identity\.
.
An encrypted file can't be linked to the native recipient it's encrypted to without access to the corresponding identity\.
.SS "SSH keys"
As a convenience feature, \fBage\fR also supports encrypting to RSA or Ed25519 ssh(1) keys\. RSA keys must be at least 2048 bits\. This feature employs more complex cryptography, and should only be used when a native key is not available for the recipient\. Note that SSH keys might not be protected long\-term by the recipient, since they are revokable when used only for authentication\.
.
.P
A \fBRECIPIENT\fR encoding is an SSH public key in \fBauthorized_keys\fR format (see the \fBAUTHORIZED_KEYS FILE FORMAT\fR section of sshd(8)), starting with \fBssh\-rsa\fR or \fBssh\-ed25519\fR, like the following:
.
.IP "" 4
.
.nf
ssh\-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDULTit0KUehbi[\.\.\.]GU4BtElAbzh8=
ssh\-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH9pO5pz22JZEas[\.\.\.]l1uZc31FGYMXa
.
ssh\-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDULTit0KUehbi[\|\.\|\.\|\.]GU4BtElAbzh8=
ssh\-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH9pO5pz22JZEas[\|\.\|\.\|\.]l1uZc31FGYMXa
.fi
.
.IP "" 0
.
.P
The comment at the end of the line, if present, is ignored\.
.
.P
In recipient files passed to \fB\-R\fR/\fB\-\-recipients\-file\fR, unsupported but valid SSH public keys are ignored with a warning, to facilitate using \fBauthorized_keys\fR or GitHub \fB\.keys\fR files\. (See \fIEXAMPLES\fR\.)
.
.P
An \fBIDENTITY\fR is an SSH private key \fIfile\fR passed individually to \fB\-i\fR/\fB\-\-identity\fR\. Note that keys held on hardware tokens such as YubiKeys or accessed via ssh\-agent(1) are not supported\.
.
.P
An encrypted file \fIcan\fR be linked to the SSH public key it was encrypted to\. This is so that \fBage\fR can identify the correct SSH private key before requesting its password, if any\.
.
.SS "Plugins"
\fBage\fR can be extended through plugins\. A plugin is only loaded if a corresponding \fBRECIPIENT\fR or \fBIDENTITY\fR is specified\. (Simply decrypting a file encrypted with a plugin will not cause it to load, for security reasons among others\.)
.
.P
A \fBRECIPIENT\fR for a plugin named \fBexample\fR starts with \fBage1example1\fR, while an \fBIDENTITY\fR starts with \fBAGE\-PLUGIN\-EXAMPLE\-1\fR\. They both encode arbitrary plugin\-specific data, and are generated by the plugin\.
.
.P
When either is specified, \fBage\fR searches for \fBage\-plugin\-example\fR in the PATH and executes it to perform the file header encryption or decryption\. The plugin may request input from the user through \fBage\fR to complete the operation\.
.
.P
Plugins can be freely mixed with other plugins or natively supported keys\.
.
.P
A plugin is not bound to only encrypt or decrypt files meant for or generated by the plugin\. For example, a plugin can be used to decrypt files encrypted to a native X25519 \fBRECIPIENT\fR or even with a passphrase\. Similarly, a plugin can encrypt a file such that it can be decrypted without the use of any plugin\.
.
.P
Plugins for which the \fBIDENTITY\fR/\fBRECIPIENT\fR distinction doesn\'t make sense (such as a symmetric encryption plugin) may generate only an \fBIDENTITY\fR and instruct the user to perform encryption with the \fB\-e\fR/\fB\-\-encrypt\fR and \fB\-i\fR/\fB\-\-identity\fR flags\. Plugins for which the concept of separate identities doesn\'t make sense (such as a password\-encryption plugin) may instruct the user to use the \fB\-j\fR flag\.
.
Plugins for which the \fBIDENTITY\fR/\fBRECIPIENT\fR distinction doesn't make sense (such as a symmetric encryption plugin) may generate only an \fBIDENTITY\fR and instruct the user to perform encryption with the \fB\-e\fR/\fB\-\-encrypt\fR and \fB\-i\fR/\fB\-\-identity\fR flags\. Plugins for which the concept of separate identities doesn't make sense (such as a password\-encryption plugin) may instruct the user to use the \fB\-j\fR flag\.
.SH "EXIT STATUS"
\fBage\fR will exit 0 if and only if encryption or decryption are successful for the full length of the input\.
.
.P
If an error occurs during decryption, partial output might still be generated, but only if it was possible to securely authenticate it\. No unauthenticathed output is ever released\.
.
If an error occurs during decryption, partial output might still be generated, but only if it was possible to securely authenticate it\. No unauthenticated output is ever released\.
.SH "BACKWARDS COMPATIBILITY"
Files encrypted with a stable version (not alpha, beta, or release candidate) of \fBage\fR, or with any v1\.0\.0 beta or release candidate, will decrypt with any later version of the tool\.
.
.P
If decrypting older files poses a security risk, doing so might cause an error by default\. In this case, a flag will be provided to force the operation\.
.
.SH "EXAMPLES"
Generate a new identity, encrypt data, and decrypt:
.
.IP "" 4
.
.nf
$ age\-keygen \-o key\.txt
Public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
$ tar cvz ~/data | age \-r age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p > data\.tar\.gz\.age
$ age \-d \-o data\.tar\.gz \-i key\.txt data\.tar\.gz\.age
.
.fi
.
.IP "" 0
.
.P
Encrypt \fBexample\.jpg\fR to multiple recipients and output to \fBexample\.jpg\.age\fR:
.
.IP "" 4
.
.nf
$ age \-o example\.jpg\.age \-r age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p \e
\-r age1lggyhqrw2nlhcxprm67z43rta597azn8gknawjehu9d9dl0jq3yqqvfafg example\.jpg
.
.fi
.
.IP "" 0
.
.P
Encrypt to a list of recipients:
.
.IP "" 4
.
.nf
$ cat > recipients\.txt
# Alice
age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
@@ -283,36 +186,24 @@ age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
age1lggyhqrw2nlhcxprm67z43rta597azn8gknawjehu9d9dl0jq3yqqvfafg
$ age \-R recipients\.txt example\.jpg > example\.jpg\.age
.
.fi
.
.IP "" 0
.
.P
Encrypt and decrypt a file using a passphrase:
.
.IP "" 4
.
.nf
$ 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:
.
.fi
.
.IP "" 0
.
.P
Encrypt and decrypt with a passphrase\-protected identity file:
.
.IP "" 4
.
.nf
$ age\-keygen | age \-p > key\.age
Public key: age1yhm4gctwfmrpz87tdslm550wrx6m79y9f2hdzt0lndjnehwj0ukqrjpyx5
Enter passphrase (leave empty to autogenerate a secure one):
@@ -322,58 +213,36 @@ $ age \-r age1yhm4gctwfmrpz87tdslm550wrx6m79y9f2hdzt0lndjnehwj0ukqrjpyx5 secrets
$ age \-d \-i key\.age secrets\.txt\.age > secrets\.txt
Enter passphrase for identity file "key\.age":
.
.fi
.
.IP "" 0
.
.P
Encrypt and decrypt with an SSH public key:
.
.IP "" 4
.
.nf
$ age \-R ~/\.ssh/id_ed25519\.pub example\.jpg > example\.jpg\.age
$ age \-d \-i ~/\.ssh/id_ed25519 example\.jpg\.age > example\.jpg
.
.fi
.
.IP "" 0
.
.P
Encrypt and decrypt with age\-plugin\-yubikey:
.
.IP "" 4
.
.nf
$ age\-plugin\-yubikey # run interactive setup, generate identity file and obtain recipient
$ age \-r age1yubikey1qwt50d05nh5vutpdzmlg5wn80xq5negm4uj9ghv0snvdd3yysf5yw3rhl3t secrets\.txt > secrets\.txt\.age
$ age \-d \-i age\-yubikey\-identity\-388178f3\.txt secrets\.txt\.age
.
.fi
.
.IP "" 0
.
.P
Encrypt to the SSH keys of a GitHub user:
.
.IP "" 4
.
.nf
$ curl https://github\.com/benjojo\.keys | age \-R \- example\.jpg > example\.jpg\.age
.
.fi
.
.IP "" 0
.
.SH "SEE ALSO"
age\-keygen(1)
.
.SH "AUTHORS"
Filippo Valsorda \fIage@filippo\.io\fR
+98 -57
View File
@@ -1,8 +1,8 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv='content-type' value='text/html;charset=utf8'>
<meta name='generator' value='Ronn/v0.7.3 (http://github.com/rtomayko/ronn/tree/0.7.3)'>
<meta http-equiv='content-type' content='text/html;charset=utf8'>
<meta name='generator' content='Ronn-NG/v0.9.1 (http://github.com/apjanke/ronn-ng/tree/0.9.1)'>
<title>age(1) - simple, modern, and secure file encryption</title>
<style type='text/css' media='all'>
/* style: man */
@@ -71,16 +71,17 @@
<li class='tr'>age(1)</li>
</ol>
<h2 id="NAME">NAME</h2>
<h2 id="NAME">NAME</h2>
<p class="man-name">
<code>age</code> - <span class="man-whatis">simple, modern, and secure file encryption</span>
</p>
<h2 id="SYNOPSIS">SYNOPSIS</h2>
<p><code>age</code> [<code>--encrypt</code>] (<code>-r</code> <var>RECIPIENT</var> | <code>-R</code> <var>PATH</var>)... [<code>--armor</code>] [<code>-o</code> <var>OUTPUT</var>] [<var>INPUT</var>]<br />
<code>age</code> [<code>--encrypt</code>] <code>--passphrase</code> [<code>--armor</code>] [<code>-o</code> <var>OUTPUT</var>] [<var>INPUT</var>]<br />
<code>age</code> <code>--decrypt</code> [<code>-i</code> <var>PATH</var> | <code>-j</code> <var>PLUGIN</var>]... [<code>-o</code> <var>OUTPUT</var>] [<var>INPUT</var>]<br /></p>
<p><code>age</code> [<code>--encrypt</code>] (<code>-r</code> <var>RECIPIENT</var> | <code>-R</code> <var>PATH</var>)... [<code>--armor</code>] [<code>-o</code> <var>OUTPUT</var>] [<var>INPUT</var>]<br>
<code>age</code> [<code>--encrypt</code>] <code>--passphrase</code> [<code>--armor</code>] [<code>-o</code> <var>OUTPUT</var>] [<var>INPUT</var>]<br>
<code>age</code> <code>--decrypt</code> [<code>-i</code> <var>PATH</var> | <code>-j</code> <var>PLUGIN</var>]... [<code>-o</code> <var>OUTPUT</var>] [<var>INPUT</var>]<br></p>
<h2 id="DESCRIPTION">DESCRIPTION</h2>
@@ -104,107 +105,148 @@ overhead per recipient, plus 16 bytes every 64KiB of plaintext.</p>
<h2 id="OPTIONS">OPTIONS</h2>
<dl>
<dt><code>-o</code>, <code>--output</code>=<var>OUTPUT</var></dt><dd><p> Write encrypted or decrypted file to <var>OUTPUT</var> instead of standard output.
If <var>OUTPUT</var> already exists it will be overwritten.</p>
<dt>
<code>-o</code>, <code>--output</code>=<var>OUTPUT</var>
</dt>
<dd> Write encrypted or decrypted file to <var>OUTPUT</var> instead of standard output.
If <var>OUTPUT</var> already exists it will be overwritten.
<p> If encrypting without <code>--armor</code>, <code>age</code> will refuse to output binary to a
TTY. This can be forced by specifying <code>-</code> as <var>OUTPUT</var>.</p></dd>
<dt><code>--version</code></dt><dd><p> Print the version and exit.</p></dd>
<p>If encrypting without <code>--armor</code>, <code>age</code> will refuse to output binary to a
TTY. This can be forced by specifying <code>-</code> as <var>OUTPUT</var>.</p>
</dd>
<dt><code>--version</code></dt>
<dd> Print the version and exit.</dd>
</dl>
<h3 id="Encryption-options">Encryption options</h3>
<dl>
<dt><code>-e</code>, <code>--encrypt</code></dt><dd><p> Encrypt <var>INPUT</var> to <var>OUTPUT</var>. This is the default.</p></dd>
<dt><code>-r</code>, <code>--recipient</code>=<var>RECIPIENT</var></dt><dd><p> Encrypt to the explicitly specified <var>RECIPIENT</var>. See the
<a href="#RECIPIENTS-AND-IDENTITIES" title="RECIPIENTS AND IDENTITIES" data-bare-link="true">RECIPIENTS AND IDENTITIES</a> section for possible recipient formats.</p>
<dt>
<code>-e</code>, <code>--encrypt</code>
</dt>
<dd> Encrypt <var>INPUT</var> to <var>OUTPUT</var>. This is the default.</dd>
<dt>
<code>-r</code>, <code>--recipient</code>=<var>RECIPIENT</var>
</dt>
<dd> Encrypt to the explicitly specified <var>RECIPIENT</var>. See the
<a href="#RECIPIENTS-AND-IDENTITIES" title="RECIPIENTS AND IDENTITIES" data-bare-link="true">RECIPIENTS AND IDENTITIES</a> section for possible recipient formats.
<p> This option can be repeated and combined with other recipient flags,
and the file can be decrypted by all provided recipients independently.</p></dd>
<dt><code>-R</code>, <code>--recipients-file</code>=<var>PATH</var></dt><dd><p> Encrypt to the <a href="#RECIPIENTS-AND-IDENTITIES" title="RECIPIENTS AND IDENTITIES" data-bare-link="true">RECIPIENTS</a> listed in the
<p>This option can be repeated and combined with other recipient flags,
and the file can be decrypted by all provided recipients independently.</p>
</dd>
<dt>
<code>-R</code>, <code>--recipients-file</code>=<var>PATH</var>
</dt>
<dd> Encrypt to the <a href="#RECIPIENTS-AND-IDENTITIES" title="RECIPIENTS AND IDENTITIES" data-bare-link="true">RECIPIENTS</a> listed in the
file at <var>PATH</var>, one per line. Empty lines and lines starting with <code>#</code>
are ignored as comments.</p>
are ignored as comments.
<p> If <var>PATH</var> is <code>-</code>, the recipients are read from standard input. In
<p>If <var>PATH</var> is <code>-</code>, the recipients are read from standard input. In
this case, the <var>INPUT</var> argument must be specified.</p>
<p> This option can be repeated and combined with other recipient flags,
and the file can be decrypted by all provided recipients independently.</p></dd>
<dt><code>-p</code>, <code>--passphrase</code></dt><dd><p> Encrypt with a passphrase, requested interactively from the terminal.
<code>age</code> will offer to auto-generate a secure passphrase.</p>
<p>This option can be repeated and combined with other recipient flags,
and the file can be decrypted by all provided recipients independently.</p>
</dd>
<dt>
<code>-p</code>, <code>--passphrase</code>
</dt>
<dd> Encrypt with a passphrase, requested interactively from the terminal.
<code>age</code> will offer to auto-generate a secure passphrase.
<p> This option can't be used with other recipient flags.</p></dd>
<dt><code>-a</code>, <code>--armor</code></dt><dd><p> Encrypt to an ASCII-only "armored" encoding.</p>
<p>This option can't be used with other recipient flags.</p>
</dd>
<dt>
<code>-a</code>, <code>--armor</code>
</dt>
<dd> Encrypt to an ASCII-only "armored" encoding.
<p> <code>age</code> armor is a strict version of PEM with type <code>AGE ENCRYPTED FILE</code>,
<p><code>age</code> armor is a strict version of PEM with type <code>AGE ENCRYPTED FILE</code>,
canonical "strict" Base64, no headers, and no support for leading and
trailing extra data.</p>
<p> Decryption transparently detects and decodes ASCII armoring.</p></dd>
<dt><code>-i</code>, <code>--identity</code>=<var>PATH</var></dt><dd><p> Encrypt to the <a href="#RECIPIENTS-AND-IDENTITIES" title="RECIPIENTS AND IDENTITIES" data-bare-link="true">RECIPIENTS</a> corresponding to the
<p>Decryption transparently detects and decodes ASCII armoring.</p>
</dd>
<dt>
<code>-i</code>, <code>--identity</code>=<var>PATH</var>
</dt>
<dd> Encrypt to the <a href="#RECIPIENTS-AND-IDENTITIES" title="RECIPIENTS AND IDENTITIES" data-bare-link="true">RECIPIENTS</a> corresponding to the
<a href="#RECIPIENTS-AND-IDENTITIES" title="RECIPIENTS AND IDENTITIES" data-bare-link="true">IDENTITIES</a> listed in the file at <var>PATH</var>. This
is equivalent to converting the file at <var>PATH</var> to a recipients file with
<code>age-keygen -y</code> and then passing that to <code>-R</code>/<code>--recipients-file</code>.</p>
<code>age-keygen -y</code> and then passing that to <code>-R</code>/<code>--recipients-file</code>.
<p> For the format of <var>PATH</var>, see the definition of <code>-i</code>/<code>--identity</code> in the
<p>For the format of <var>PATH</var>, see the definition of <code>-i</code>/<code>--identity</code> in the
<a href="#Decryption-options" title="Decryption options" data-bare-link="true">Decryption options</a> section.</p>
<p> <code>-e</code>/<code>--encrypt</code> must be explicitly specified when using <code>-i</code>/<code>--identity</code>
in encryption mode to avoid confusion.</p></dd>
<dt><code>-j</code> <var>PLUGIN</var></dt><dd><p> Encrypt using the data-less <a href="#Plugins" title="Plugins" data-bare-link="true">plugin</a> <var>PLUGIN</var>.</p>
<p><code>-e</code>/<code>--encrypt</code> must be explicitly specified when using <code>-i</code>/<code>--identity</code>
in encryption mode to avoid confusion.</p>
</dd>
<dt>
<code>-j</code> <var>PLUGIN</var>
</dt>
<dd> Encrypt using the data-less <a href="#Plugins" title="Plugins" data-bare-link="true">plugin</a> <var>PLUGIN</var>.
<p> This is equivalent to using <code>-i</code>/<code>--identity</code> with a file that contains a
<p>This is equivalent to using <code>-i</code>/<code>--identity</code> with a file that contains a
single plugin <code>IDENTITY</code> that encodes no plugin-specific data.</p>
<p> <code>-e</code>/<code>--encrypt</code> must be explicitly specified when using <code>-j</code> in encryption
mode to avoid confusion.</p></dd>
<p><code>-e</code>/<code>--encrypt</code> must be explicitly specified when using <code>-j</code> in encryption
mode to avoid confusion.</p>
</dd>
</dl>
<h3 id="Decryption-options">Decryption options</h3>
<dl>
<dt><code>-d</code>, <code>--decrypt</code></dt><dd><p> Decrypt <var>INPUT</var> to <var>OUTPUT</var>.</p>
<dt>
<code>-d</code>, <code>--decrypt</code>
</dt>
<dd> Decrypt <var>INPUT</var> to <var>OUTPUT</var>.
<p> If <var>INPUT</var> is passphrase encrypted, it will be automatically detected
<p>If <var>INPUT</var> is passphrase encrypted, it will be automatically detected
and the passphrase will be requested interactively. Otherwise, the
<a href="#RECIPIENTS-AND-IDENTITIES" title="RECIPIENTS AND IDENTITIES" data-bare-link="true">IDENTITIES</a> specified with <code>-i</code>/<code>--identity</code>
are used.</p>
<p> ASCII armoring is transparently detected and decoded.</p></dd>
<dt><code>-i</code>, <code>--identity</code>=<var>PATH</var></dt><dd><p> Decrypt using the <a href="#RECIPIENTS-AND-IDENTITIES" title="RECIPIENTS AND IDENTITIES" data-bare-link="true">IDENTITIES</a> at <var>PATH</var>.</p>
<p>ASCII armoring is transparently detected and decoded.</p>
</dd>
<dt>
<code>-i</code>, <code>--identity</code>=<var>PATH</var>
</dt>
<dd> Decrypt using the <a href="#RECIPIENTS-AND-IDENTITIES" title="RECIPIENTS AND IDENTITIES" data-bare-link="true">IDENTITIES</a> at <var>PATH</var>.
<p> <var>PATH</var> may be one of the following:</p>
<p><var>PATH</var> may be one of the following:</p>
<p> a. A file listing <a href="#RECIPIENTS-AND-IDENTITIES" title="RECIPIENTS AND IDENTITIES" data-bare-link="true">IDENTITIES</a> one per line.
<p>a. A file listing <a href="#RECIPIENTS-AND-IDENTITIES" title="RECIPIENTS AND IDENTITIES" data-bare-link="true">IDENTITIES</a> one per line.
Empty lines and lines starting with "<code>#</code>" are ignored as comments.</p>
<p> b. A passphrase encrypted age file, containing
<p>b. A passphrase encrypted age file, containing
<a href="#RECIPIENTS-AND-IDENTITIES" title="RECIPIENTS AND IDENTITIES" data-bare-link="true">IDENTITIES</a> one per line like above.
The passphrase is requested interactively. Note that passphrase-protected
identity files are not necessary for most use cases, where access to the
encrypted identity file implies access to the whole system.</p>
<p> c. An SSH private key file, in PKCS#1, PKCS#8, or OpenSSH format.
<p>c. An SSH private key file, in PKCS#1, PKCS#8, or OpenSSH format.
If the private key is password-protected, the password is requested
interactively only if the SSH identity matches the file. See the
<a href="#SSH-keys" title="SSH keys" data-bare-link="true">SSH keys</a> section for more information, including supported key types.</p>
<p> d. "<code>-</code>", causing one of the options above to be read from standard input.
<p>d. "<code>-</code>", causing one of the options above to be read from standard input.
In this case, the <var>INPUT</var> argument must be specified.</p>
<p> This option can be repeated. Identities are tried in the order in which are
<p>This option can be repeated. Identities are tried in the order in which are
provided, and the first one matching one of the file's recipients is used.
Unused identities are ignored, but it is an error if the <var>INPUT</var> file is
passphrase-encrypted and <code>-i</code>/<code>--identity</code> is specified.</p></dd>
<dt><code>-j</code> <var>PLUGIN</var></dt><dd><p> Decrypt using the data-less <a href="#Plugins" title="Plugins" data-bare-link="true">plugin</a> <var>PLUGIN</var>.</p>
passphrase-encrypted and <code>-i</code>/<code>--identity</code> is specified.</p>
</dd>
<dt>
<code>-j</code> <var>PLUGIN</var>
</dt>
<dd> Decrypt using the data-less <a href="#Plugins" title="Plugins" data-bare-link="true">plugin</a> <var>PLUGIN</var>.
<p> This is equivalent to using <code>-i</code>/<code>--identity</code> with a file that contains a
single plugin <code>IDENTITY</code> that encodes no plugin-specific data.</p></dd>
<p>This is equivalent to using <code>-i</code>/<code>--identity</code> with a file that contains a
single plugin <code>IDENTITY</code> that encodes no plugin-specific data.</p>
</dd>
</dl>
<h2 id="RECIPIENTS-AND-IDENTITIES">RECIPIENTS AND IDENTITIES</h2>
<p><code>RECIPIENTS</code> are public values, like a public key, that a file can be encrypted
@@ -295,7 +337,7 @@ to use the <code>-j</code> flag.</p>
full length of the input.</p>
<p>If an error occurs during decryption, partial output might still be generated,
but only if it was possible to securely authenticate it. No unauthenticathed
but only if it was possible to securely authenticate it. No unauthenticated
output is ever released.</p>
<h2 id="BACKWARDS-COMPATIBILITY">BACKWARDS COMPATIBILITY</h2>
@@ -388,10 +430,9 @@ $ age -d -i age-yubikey-identity-388178f3.txt secrets.txt.age
<p>Filippo Valsorda <a href="mailto:age@filippo.io" data-bare-link="true">age@filippo.io</a></p>
<ol class='man-decor man-foot man foot'>
<li class='tl'></li>
<li class='tc'>June 2022</li>
<li class='tc'>April 2023</li>
<li class='tr'>age(1)</li>
</ol>
+3 -3
View File
@@ -69,7 +69,7 @@ overhead per recipient, plus 16 bytes every 64KiB of plaintext.
* `-a`, `--armor`:
Encrypt to an ASCII-only "armored" encoding.
`age` armor is a strict version of PEM with type `AGE ENCRYPTED FILE`,
canonical "strict" Base64, no headers, and no support for leading and
trailing extra data.
@@ -127,7 +127,7 @@ overhead per recipient, plus 16 bytes every 64KiB of plaintext.
If the private key is password-protected, the password is requested
interactively only if the SSH identity matches the file. See the
[SSH keys][] section for more information, including supported key types.
d\. "`-`", causing one of the options above to be read from standard input.
In this case, the <INPUT> argument must be specified.
@@ -229,7 +229,7 @@ to use the `-j` flag.
full length of the input.
If an error occurs during decryption, partial output might still be generated,
but only if it was possible to securely authenticate it. No unauthenticathed
but only if it was possible to securely authenticate it. No unauthenticated
output is ever released.
## BACKWARDS COMPATIBILITY
+12 -5
View File
@@ -1,10 +1,17 @@
module filippo.io/age
go 1.17
go 1.19
require (
filippo.io/edwards25519 v1.0.0-rc.1
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5
golang.org/x/sys v0.0.0-20210903071746-97244b99971b
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b
filippo.io/edwards25519 v1.1.0
golang.org/x/crypto v0.24.0
golang.org/x/sys v0.21.0
golang.org/x/term v0.21.0
)
// Test dependencies.
require (
c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805
github.com/rogpeppe/go-internal v1.12.0
golang.org/x/tools v0.22.0 // indirect
)
+14 -14
View File
@@ -1,14 +1,14 @@
filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU=
filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210903071746-97244b99971b h1:3Dq0eVHn0uaQJmPO+/aYPI/fRMqdrVDbu7MQcku54gg=
golang.org/x/sys v0.0.0-20210903071746-97244b99971b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE=
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805 h1:u2qwJeEvnypw+OCPUHmoZE3IqwfuN5kgDfo5MLzpNM0=
c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805/go.mod h1:FomMrUJ2Lxt5jCLmZkG3FHa72zUprnhd3v/Z18Snm4w=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
-6
View File
@@ -111,9 +111,6 @@ func Encode(hrp string, data []byte) (string, error) {
if err != nil {
return "", err
}
if len(hrp)+len(values)+7 > 90 {
return "", fmt.Errorf("too long: hrp length=%d, data length=%d", len(hrp), len(values))
}
if len(hrp) < 1 {
return "", fmt.Errorf("invalid HRP: %q", hrp)
}
@@ -144,9 +141,6 @@ func Encode(hrp string, data []byte) (string, error) {
// Decode decodes a Bech32 string. If the string is uppercase, the HRP will be uppercase.
func Decode(s string) (hrp string, data []byte, err error) {
if len(s) > 90 {
return "", nil, fmt.Errorf("too long: len=%d", len(s))
}
if strings.ToLower(s) != s && strings.ToUpper(s) != s {
return "", nil, fmt.Errorf("mixed case")
}
+5 -4
View File
@@ -28,7 +28,7 @@ func TestBech32(t *testing.T) {
str string
valid bool
}{
{"A12UEL5L", true},
{"A12UEL5L", true}, // empty
{"a12uel5l", true},
{"an83characterlonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1tt5tgs", true},
{"abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw", true},
@@ -44,11 +44,12 @@ func TestBech32(t *testing.T) {
{"1checkupstagehandshakeupstreamerranterredcaperred2y9e3w", false}, // empty hrp
// invalid character (DEL) in hrp
{"spl" + string(rune(127)) + "t1checkupstagehandshakeupstreamerranterredcaperred2y9e3w", false},
// too long
{"11qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqsqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqc8247j", false},
// long vectors that we do accept despite the spec, see Issue 453
{"long10pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7qfcsvr0", true},
{"an84characterslonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1569pvx", true},
// BIP 173 invalid vectors.
{"an84characterslonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1569pvx", false},
{"pzry9x0s0muk", false},
{"1pzry9x0s0muk", false},
{"x1b4n0q5v", false},
+17 -11
View File
@@ -175,7 +175,7 @@ func (r *StanzaReader) ReadStanza() (s *Stanza, err error) {
line, err := r.r.ReadBytes('\n')
if err != nil {
return nil, fmt.Errorf("failed to read line: %v", err)
return nil, fmt.Errorf("failed to read line: %w", err)
}
if !bytes.HasPrefix(line, stanzaPrefix) {
return nil, fmt.Errorf("malformed stanza opening line: %q", line)
@@ -195,7 +195,7 @@ func (r *StanzaReader) ReadStanza() (s *Stanza, err error) {
for {
line, err := r.r.ReadBytes('\n')
if err != nil {
return nil, fmt.Errorf("failed to read line: %v", err)
return nil, fmt.Errorf("failed to read line: %w", err)
}
b, err := DecodeString(strings.TrimSuffix(string(line), "\n"))
@@ -216,14 +216,20 @@ func (r *StanzaReader) ReadStanza() (s *Stanza, err error) {
}
}
type ParseError string
type ParseError struct {
err error
}
func (e ParseError) Error() string {
return "parsing age header: " + string(e)
func (e *ParseError) Error() string {
return "parsing age header: " + e.err.Error()
}
func (e *ParseError) Unwrap() error {
return e.err
}
func errorf(format string, a ...interface{}) error {
return ParseError(fmt.Sprintf(format, a...))
return &ParseError{fmt.Errorf(format, a...)}
}
// Parse returns the header and a Reader that begins at the start of the
@@ -234,7 +240,7 @@ func Parse(input io.Reader) (*Header, io.Reader, error) {
line, err := rr.ReadString('\n')
if err != nil {
return nil, nil, errorf("failed to read intro: %v", err)
return nil, nil, errorf("failed to read intro: %w", err)
}
if line != intro {
return nil, nil, errorf("unexpected intro: %q", line)
@@ -244,13 +250,13 @@ func Parse(input io.Reader) (*Header, io.Reader, error) {
for {
peek, err := rr.Peek(len(footerPrefix))
if err != nil {
return nil, nil, errorf("failed to read header: %v", err)
return nil, nil, errorf("failed to read header: %w", err)
}
if bytes.Equal(peek, footerPrefix) {
line, err := rr.ReadBytes('\n')
if err != nil {
return nil, nil, fmt.Errorf("failed to read header: %v", err)
return nil, nil, fmt.Errorf("failed to read header: %w", err)
}
prefix, args := splitArgs(line)
@@ -258,7 +264,7 @@ func Parse(input io.Reader) (*Header, io.Reader, error) {
return nil, nil, errorf("malformed closing line: %q", line)
}
h.MAC, err = DecodeString(args[0])
if err != nil {
if err != nil || len(h.MAC) != 32 {
return nil, nil, errorf("malformed closing line %q: %v", line, err)
}
break
@@ -266,7 +272,7 @@ func Parse(input io.Reader) (*Header, io.Reader, error) {
s, err := sr.ReadStanza()
if err != nil {
return nil, nil, fmt.Errorf("failed to parse header: %v", err)
return nil, nil, fmt.Errorf("failed to parse header: %w", err)
}
h.Recipients = append(h.Recipients, s)
}
+46
View File
@@ -2,10 +2,16 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build go1.18
// +build go1.18
package format_test
import (
"bytes"
"io"
"os"
"path/filepath"
"testing"
"filippo.io/age/internal/format"
@@ -37,3 +43,43 @@ func TestStanzaMarshal(t *testing.T) {
t.Errorf("wrong 64 columns stanza encoding: expected %q, got %q", exp, buf.String())
}
}
func FuzzMalleability(f *testing.F) {
tests, err := filepath.Glob("../../testdata/testkit/*")
if err != nil {
f.Fatal(err)
}
for _, test := range tests {
contents, err := os.ReadFile(test)
if err != nil {
f.Fatal(err)
}
_, contents, ok := bytes.Cut(contents, []byte("\n\n"))
if !ok {
f.Fatal("testkit file without header")
}
f.Add(contents)
}
f.Fuzz(func(t *testing.T, data []byte) {
h, payload, err := format.Parse(bytes.NewReader(data))
if err != nil {
if h != nil {
t.Error("h != nil on error")
}
if payload != nil {
t.Error("payload != nil on error")
}
t.Skip()
}
w := &bytes.Buffer{}
if err := h.Marshal(w); err != nil {
t.Fatal(err)
}
if _, err := io.Copy(w, payload); err != nil {
t.Fatal(err)
}
if !bytes.Equal(w.Bytes(), data) {
t.Error("Marshal output different from input")
}
})
}
+12 -1
View File
@@ -8,6 +8,7 @@ package stream
import (
"crypto/cipher"
"errors"
"fmt"
"io"
"golang.org/x/crypto/chacha20poly1305"
@@ -66,7 +67,17 @@ func (r *Reader) Read(p []byte) (int, error) {
r.unread = r.unread[n:]
if last {
r.err = io.EOF
// Ensure there is an EOF after the last chunk as expected. In other
// words, check for trailing data after a full-length final chunk.
// Hopefully, the underlying reader supports returning EOF even if it
// had previously returned an EOF to ReadFull.
if _, err := r.src.Read(make([]byte, 1)); err == nil {
r.err = errors.New("trailing data after end of encrypted file")
} else if err != io.EOF {
r.err = fmt.Errorf("non-EOF error reading after end of encrypted file: %w", err)
} else {
r.err = io.EOF
}
}
return n, nil
+53 -35
View File
@@ -12,15 +12,15 @@ import (
"bytes"
"fmt"
"io"
"math/rand"
"os"
"path/filepath"
"strconv"
"strings"
"time"
exec "golang.org/x/sys/execabs"
"filippo.io/age"
"filippo.io/age/internal/bech32"
"filippo.io/age/internal/format"
)
@@ -34,16 +34,13 @@ type Recipient struct {
}
var _ age.Recipient = &Recipient{}
var _ age.RecipientWithLabels = &Recipient{}
func NewRecipient(s string, ui *ClientUI) (*Recipient, error) {
hrp, _, err := bech32.Decode(s)
name, _, err := ParseRecipient(s)
if err != nil {
return nil, fmt.Errorf("invalid recipient encoding %q: %v", s, err)
return nil, err
}
if !strings.HasPrefix(hrp, "age1") {
return nil, fmt.Errorf("not a plugin recipient %q: %v", s, err)
}
name := strings.TrimPrefix(hrp, "age1")
return &Recipient{
name: name, encoding: s, ui: ui,
}, nil
@@ -57,6 +54,11 @@ func (r *Recipient) Name() string {
}
func (r *Recipient) Wrap(fileKey []byte) (stanzas []*age.Stanza, err error) {
stanzas, _, err = r.WrapWithLabels(fileKey)
return
}
func (r *Recipient) WrapWithLabels(fileKey []byte) (stanzas []*age.Stanza, labels []string, err error) {
defer func() {
if err != nil {
err = fmt.Errorf("%s plugin: %w", r.name, err)
@@ -65,7 +67,7 @@ func (r *Recipient) Wrap(fileKey []byte) (stanzas []*age.Stanza, err error) {
conn, err := openClientConnection(r.name, "recipient-v1")
if err != nil {
return nil, fmt.Errorf("couldn't start plugin: %v", err)
return nil, nil, fmt.Errorf("couldn't start plugin: %v", err)
}
defer conn.Close()
@@ -75,13 +77,19 @@ func (r *Recipient) Wrap(fileKey []byte) (stanzas []*age.Stanza, err error) {
addType = "add-identity"
}
if err := writeStanza(conn, addType, r.encoding); err != nil {
return nil, err
return nil, nil, err
}
if err := writeStanza(conn, fmt.Sprintf("grease-%x", rand.Int())); err != nil {
return nil, nil, err
}
if err := writeStanzaWithBody(conn, "wrap-file-key", fileKey); err != nil {
return nil, err
return nil, nil, err
}
if err := writeStanza(conn, "extension-labels"); err != nil {
return nil, nil, err
}
if err := writeStanza(conn, "done"); err != nil {
return nil, err
return nil, nil, err
}
// Phase 2: plugin responds with stanzas
@@ -90,21 +98,21 @@ ReadLoop:
for {
s, err := r.ui.readStanza(r.name, sr)
if err != nil {
return nil, err
return nil, nil, err
}
switch s.Type {
case "recipient-stanza":
if len(s.Args) < 2 {
return nil, fmt.Errorf("malformed recipient stanza: unexpected argument count")
return nil, nil, fmt.Errorf("malformed recipient stanza: unexpected argument count")
}
n, err := strconv.Atoi(s.Args[0])
if err != nil {
return nil, fmt.Errorf("malformed recipient stanza: invalid index")
return nil, nil, fmt.Errorf("malformed recipient stanza: invalid index")
}
// We only send a single file key, so the index must be 0.
if n != 0 {
return nil, fmt.Errorf("malformed recipient stanza: unexpected index")
return nil, nil, fmt.Errorf("malformed recipient stanza: unexpected index")
}
stanzas = append(stanzas, &age.Stanza{
@@ -114,32 +122,41 @@ ReadLoop:
})
if err := writeStanza(conn, "ok"); err != nil {
return nil, err
return nil, nil, err
}
case "labels":
if labels != nil {
return nil, nil, fmt.Errorf("repeated labels stanza")
}
labels = s.Args
if err := writeStanza(conn, "ok"); err != nil {
return nil, nil, err
}
case "error":
if err := writeStanza(conn, "ok"); err != nil {
return nil, err
return nil, nil, err
}
return nil, fmt.Errorf("%s", s.Body)
return nil, nil, fmt.Errorf("%s", s.Body)
case "done":
break ReadLoop
default:
if ok, err := r.ui.handle(r.name, conn, s); err != nil {
return nil, err
return nil, nil, err
} else if !ok {
if err := writeStanza(conn, "unsupported"); err != nil {
return nil, err
return nil, nil, err
}
}
}
}
if len(stanzas) == 0 {
return nil, fmt.Errorf("received zero recipient stanzas")
return nil, nil, fmt.Errorf("received zero recipient stanzas")
}
return stanzas, nil
return stanzas, labels, nil
}
type Identity struct {
@@ -151,25 +168,17 @@ type Identity struct {
var _ age.Identity = &Identity{}
func NewIdentity(s string, ui *ClientUI) (*Identity, error) {
hrp, _, err := bech32.Decode(s)
name, _, err := ParseIdentity(s)
if err != nil {
return nil, fmt.Errorf("invalid identity encoding: %v", err)
return nil, err
}
if !strings.HasPrefix(hrp, "AGE-PLUGIN-") || !strings.HasSuffix(hrp, "-") {
return nil, fmt.Errorf("not a plugin identity: %v", err)
}
name := strings.TrimSuffix(strings.TrimPrefix(hrp, "AGE-PLUGIN-"), "-")
name = strings.ToLower(name)
return &Identity{
name: name, encoding: s, ui: ui,
}, nil
}
func NewIdentityWithoutData(name string, ui *ClientUI) (*Identity, error) {
s, err := bech32.Encode("AGE-PLUGIN-"+strings.ToUpper(name)+"-", nil)
if err != nil {
return nil, err
}
s := EncodeIdentity(name, nil)
return &Identity{
name: name, encoding: s, ui: ui,
}, nil
@@ -211,6 +220,9 @@ func (i *Identity) Unwrap(stanzas []*age.Stanza) (fileKey []byte, err error) {
if err := writeStanza(conn, "add-identity", i.encoding); err != nil {
return nil, err
}
if err := writeStanza(conn, fmt.Sprintf("grease-%x", rand.Int())); err != nil {
return nil, err
}
for _, rs := range stanzas {
s := &format.Stanza{
Type: "recipient-stanza",
@@ -374,8 +386,14 @@ type clientConnection struct {
close func()
}
var testOnlyPluginPath string
func openClientConnection(name, protocol string) (*clientConnection, error) {
cmd := exec.Command("age-plugin-"+name, "--age-plugin="+protocol)
path := "age-plugin-" + name
if testOnlyPluginPath != "" {
path = filepath.Join(testOnlyPluginPath, path)
}
cmd := exec.Command(path, "--age-plugin="+protocol)
stdout, err := cmd.StdoutPipe()
if err != nil {
+133
View File
@@ -0,0 +1,133 @@
// Copyright 2023 The age Authors
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
package plugin
import (
"bufio"
"io"
"os"
"path/filepath"
"runtime"
"testing"
"filippo.io/age"
"filippo.io/age/internal/bech32"
)
func TestMain(m *testing.M) {
switch filepath.Base(os.Args[0]) {
// TODO: deduplicate from cmd/age TestMain.
case "age-plugin-test":
switch os.Args[1] {
case "--age-plugin=recipient-v1":
scanner := bufio.NewScanner(os.Stdin)
scanner.Scan() // add-recipient
scanner.Scan() // body
scanner.Scan() // grease
scanner.Scan() // body
scanner.Scan() // wrap-file-key
scanner.Scan() // body
fileKey := scanner.Text()
scanner.Scan() // extension-labels
scanner.Scan() // body
scanner.Scan() // done
scanner.Scan() // body
os.Stdout.WriteString("-> recipient-stanza 0 test\n")
os.Stdout.WriteString(fileKey + "\n")
scanner.Scan() // ok
scanner.Scan() // body
os.Stdout.WriteString("-> done\n\n")
os.Exit(0)
default:
panic(os.Args[1])
}
case "age-plugin-testpqc":
switch os.Args[1] {
case "--age-plugin=recipient-v1":
scanner := bufio.NewScanner(os.Stdin)
scanner.Scan() // add-recipient
scanner.Scan() // body
scanner.Scan() // grease
scanner.Scan() // body
scanner.Scan() // wrap-file-key
scanner.Scan() // body
fileKey := scanner.Text()
scanner.Scan() // extension-labels
scanner.Scan() // body
scanner.Scan() // done
scanner.Scan() // body
os.Stdout.WriteString("-> recipient-stanza 0 test\n")
os.Stdout.WriteString(fileKey + "\n")
scanner.Scan() // ok
scanner.Scan() // body
os.Stdout.WriteString("-> labels postquantum\n\n")
scanner.Scan() // ok
scanner.Scan() // body
os.Stdout.WriteString("-> done\n\n")
os.Exit(0)
default:
panic(os.Args[1])
}
default:
os.Exit(m.Run())
}
}
func TestLabels(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Windows support is TODO")
}
temp := t.TempDir()
testOnlyPluginPath = temp
t.Cleanup(func() { testOnlyPluginPath = "" })
ex, err := os.Executable()
if err != nil {
t.Fatal(err)
}
if err := os.Link(ex, filepath.Join(temp, "age-plugin-test")); err != nil {
t.Fatal(err)
}
if err := os.Chmod(filepath.Join(temp, "age-plugin-test"), 0755); err != nil {
t.Fatal(err)
}
if err := os.Link(ex, filepath.Join(temp, "age-plugin-testpqc")); err != nil {
t.Fatal(err)
}
if err := os.Chmod(filepath.Join(temp, "age-plugin-testpqc"), 0755); err != nil {
t.Fatal(err)
}
name, err := bech32.Encode("age1test", nil)
if err != nil {
t.Fatal(err)
}
testPlugin, err := NewRecipient(name, &ClientUI{})
if err != nil {
t.Fatal(err)
}
namePQC, err := bech32.Encode("age1testpqc", nil)
if err != nil {
t.Fatal(err)
}
testPluginPQC, err := NewRecipient(namePQC, &ClientUI{})
if err != nil {
t.Fatal(err)
}
if _, err := age.Encrypt(io.Discard, testPluginPQC); err != nil {
t.Errorf("expected one pqc to work, got %v", err)
}
if _, err := age.Encrypt(io.Discard, testPluginPQC, testPluginPQC); err != nil {
t.Errorf("expected two pqc to work, got %v", err)
}
if _, err := age.Encrypt(io.Discard, testPluginPQC, testPlugin); err == nil {
t.Errorf("expected one pqc and one normal to fail")
}
if _, err := age.Encrypt(io.Discard, testPlugin, testPluginPQC); err == nil {
t.Errorf("expected one pqc and one normal to fail")
}
}
+55
View File
@@ -0,0 +1,55 @@
// Copyright 2023 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package plugin
import (
"fmt"
"strings"
"filippo.io/age/internal/bech32"
)
// EncodeIdentity encodes a plugin identity string for a plugin with the given
// name. If the name is invalid, it returns an empty string.
func EncodeIdentity(name string, data []byte) string {
s, _ := bech32.Encode("AGE-PLUGIN-"+strings.ToUpper(name)+"-", data)
return s
}
// ParseIdentity decodes a plugin identity string. It returns the plugin name
// in lowercase and the encoded data.
func ParseIdentity(s string) (name string, data []byte, err error) {
hrp, data, err := bech32.Decode(s)
if err != nil {
return "", nil, fmt.Errorf("invalid identity encoding: %v", err)
}
if !strings.HasPrefix(hrp, "AGE-PLUGIN-") || !strings.HasSuffix(hrp, "-") {
return "", nil, fmt.Errorf("not a plugin identity: %v", err)
}
name = strings.TrimSuffix(strings.TrimPrefix(hrp, "AGE-PLUGIN-"), "-")
name = strings.ToLower(name)
return name, data, nil
}
// EncodeRecipient encodes a plugin recipient string for a plugin with the given
// name. If the name is invalid, it returns an empty string.
func EncodeRecipient(name string, data []byte) string {
s, _ := bech32.Encode("age1"+strings.ToLower(name), data)
return s
}
// ParseRecipient decodes a plugin recipient string. It returns the plugin name
// in lowercase and the encoded data.
func ParseRecipient(s string) (name string, data []byte, err error) {
hrp, data, err := bech32.Decode(s)
if err != nil {
return "", nil, fmt.Errorf("invalid recipient encoding: %v", err)
}
if !strings.HasPrefix(hrp, "age1") {
return "", nil, fmt.Errorf("not a plugin recipient: %v", err)
}
name = strings.TrimPrefix(hrp, "age1")
return name, data, nil
}
+24
View File
@@ -0,0 +1,24 @@
// Copyright 2023 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build go1.20
package plugin
import (
"crypto/ecdh"
"fmt"
"filippo.io/age/internal/bech32"
)
// EncodeX25519Recipient encodes a native X25519 recipient from a
// [crypto/ecdh.X25519] public key. It's meant for plugins that implement
// identities that are compatible with native recipients.
func EncodeX25519Recipient(pk *ecdh.PublicKey) (string, error) {
if pk.Curve() != ecdh.X25519() {
return "", fmt.Errorf("wrong ecdh Curve")
}
return bech32.Encode("age", pk.Bytes())
}
+32 -2
View File
@@ -6,8 +6,10 @@ package age
import (
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"regexp"
"strconv"
"filippo.io/age/internal/format"
@@ -86,6 +88,29 @@ func (r *ScryptRecipient) Wrap(fileKey []byte) ([]*Stanza, error) {
return []*Stanza{l}, nil
}
// 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.
//
// [authenticated]: https://words.filippo.io/dispatches/age-authentication/
func (r *ScryptRecipient) WrapWithLabels(fileKey []byte) (stanzas []*Stanza, labels []string, err error) {
stanzas, err = r.Wrap(fileKey)
random := make([]byte, 16)
if _, err := rand.Read(random); err != nil {
return nil, nil, err
}
labels = []string{hex.EncodeToString(random)}
return
}
// ScryptIdentity is a password-based identity.
type ScryptIdentity struct {
password []byte
@@ -128,6 +153,8 @@ func (i *ScryptIdentity) Unwrap(stanzas []*Stanza) ([]byte, error) {
return multiUnwrap(i.unwrap, stanzas)
}
var digitsRe = regexp.MustCompile(`^[1-9][0-9]*$`)
func (i *ScryptIdentity) unwrap(block *Stanza) ([]byte, error) {
if block.Type != "scrypt" {
return nil, ErrIncorrectIdentity
@@ -142,6 +169,9 @@ func (i *ScryptIdentity) unwrap(block *Stanza) ([]byte, error) {
if len(salt) != scryptSaltSize {
return nil, errors.New("invalid scrypt recipient block")
}
if w := block.Args[1]; !digitsRe.MatchString(w) {
return nil, fmt.Errorf("scrypt work factor encoding invalid: %q", w)
}
logN, err := strconv.Atoi(block.Args[1])
if err != nil {
return nil, fmt.Errorf("failed to parse scrypt work factor: %v", err)
@@ -149,13 +179,13 @@ func (i *ScryptIdentity) unwrap(block *Stanza) ([]byte, error) {
if logN > i.maxWorkFactor {
return nil, fmt.Errorf("scrypt work factor too large: %v", logN)
}
if logN <= 0 {
if logN <= 0 { // unreachable
return nil, fmt.Errorf("invalid scrypt work factor: %v", logN)
}
salt = append([]byte(scryptLabel), salt...)
k, err := scrypt.Key(i.password, salt, 1<<logN, 8, 1, chacha20poly1305.KeySize)
if err != nil {
if err != nil { // unreachable
return nil, fmt.Errorf("failed to generate scrypt hash: %v", err)
}
View File
File renamed without changes.
+274
View File
@@ -0,0 +1,274 @@
// Copyright 2022 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build go1.18
// +build go1.18
package age_test
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"errors"
"io"
"io/fs"
"strings"
"testing"
"filippo.io/age"
"filippo.io/age/armor"
"filippo.io/age/internal/format"
"filippo.io/age/internal/stream"
"golang.org/x/crypto/chacha20poly1305"
"golang.org/x/crypto/hkdf"
agetest "c2sp.org/CCTV/age"
)
func forEachVector(t *testing.T, f func(t *testing.T, v *vector)) {
tests, err := fs.ReadDir(agetest.Vectors, ".")
if err != nil {
t.Fatal(err)
}
for _, test := range tests {
name := test.Name()
contents, err := fs.ReadFile(agetest.Vectors, name)
if err != nil {
t.Fatal(err)
}
t.Run(name, func(t *testing.T) {
t.Parallel()
f(t, parseVector(t, contents))
})
}
}
type vector struct {
expect string
payloadHash *[32]byte
fileKey *[16]byte
identities []age.Identity
armored bool
file []byte
}
func parseVector(t *testing.T, test []byte) *vector {
v := &vector{file: test}
for {
line, rest, ok := bytes.Cut(v.file, []byte("\n"))
if !ok {
t.Fatal("invalid test file: no payload")
}
v.file = rest
if len(line) == 0 {
break
}
key, value, _ := strings.Cut(string(line), ": ")
switch key {
case "expect":
switch value {
case "success":
case "HMAC failure":
case "header failure":
case "armor failure":
case "payload failure":
case "no match":
default:
t.Fatal("invalid test file: unknown expect value:", value)
}
v.expect = value
case "payload":
h, err := hex.DecodeString(value)
if err != nil {
t.Fatal(err)
}
v.payloadHash = (*[32]byte)(h)
case "file key":
h, err := hex.DecodeString(value)
if err != nil {
t.Fatal(err)
}
v.fileKey = (*[16]byte)(h)
case "identity":
i, err := age.ParseX25519Identity(value)
if err != nil {
t.Fatal(err)
}
v.identities = append(v.identities, i)
case "passphrase":
i, err := age.NewScryptIdentity(value)
if err != nil {
t.Fatal(err)
}
v.identities = append(v.identities, i)
case "armored":
v.armored = true
case "comment":
t.Log(value)
default:
t.Fatal("invalid test file: unknown header key:", key)
}
}
return v
}
func TestVectors(t *testing.T) {
forEachVector(t, testVector)
}
func testVector(t *testing.T, v *vector) {
var in io.Reader = bytes.NewReader(v.file)
if v.armored {
in = armor.NewReader(in)
}
r, err := age.Decrypt(in, v.identities...)
if err != nil && strings.HasSuffix(err.Error(), "bad header MAC") {
if v.expect == "HMAC failure" {
t.Log(err)
return
}
t.Fatalf("expected %s, got HMAC error", v.expect)
} else if e := new(armor.Error); errors.As(err, &e) {
if v.expect == "armor failure" {
t.Log(err)
return
}
t.Fatalf("expected %s, got: %v", v.expect, err)
} else if _, ok := err.(*age.NoIdentityMatchError); ok {
if v.expect == "no match" {
t.Log(err)
return
}
t.Fatalf("expected %s, got: %v", v.expect, err)
} else if err != nil {
if v.expect == "header failure" {
t.Log(err)
return
}
t.Fatalf("expected %s, got: %v", v.expect, err)
} else if v.expect != "success" && v.expect != "payload failure" &&
v.expect != "armor failure" {
t.Fatalf("expected %s, got success", v.expect)
}
out, err := io.ReadAll(r)
if err != nil && v.expect == "success" {
t.Fatalf("expected %s, got: %v", v.expect, err)
} else if err != nil {
t.Log(err)
if v.expect == "armor failure" {
if e := new(armor.Error); !errors.As(err, &e) {
t.Errorf("expected armor.Error, got %T", err)
}
}
if v.payloadHash != nil && sha256.Sum256(out) != *v.payloadHash {
t.Error("partial payload hash mismatch")
}
return
} else if v.expect != "success" {
t.Fatalf("expected %s, got success", v.expect)
}
if sha256.Sum256(out) != *v.payloadHash {
t.Error("payload hash mismatch")
}
}
// TestVectorsRoundTrip checks that any (valid) armor, header, and/or STREAM
// payload in the test vectors re-encodes identically.
func TestVectorsRoundTrip(t *testing.T) {
forEachVector(t, testVectorRoundTrip)
}
func testVectorRoundTrip(t *testing.T, v *vector) {
if v.armored {
if v.expect == "armor failure" {
t.SkipNow()
}
t.Run("armor", func(t *testing.T) {
payload, err := io.ReadAll(armor.NewReader(bytes.NewReader(v.file)))
if err != nil {
t.Fatal(err)
}
buf := &bytes.Buffer{}
w := armor.NewWriter(buf)
if _, err := w.Write(payload); err != nil {
t.Fatal(err)
}
if err := w.Close(); err != nil {
t.Fatal(err)
}
// Armor format is not perfectly strict: CRLF ↔ LF and trailing and
// leading spaces are allowed and won't round-trip.
expect := bytes.Replace(v.file, []byte("\r\n"), []byte("\n"), -1)
expect = bytes.TrimSpace(expect)
expect = append(expect, '\n')
if !bytes.Equal(buf.Bytes(), expect) {
t.Error("got a different armor encoding")
}
})
// Armor tests are not interesting beyond their armor encoding.
return
}
if v.expect == "header failure" {
t.SkipNow()
}
hdr, p, err := format.Parse(bytes.NewReader(v.file))
if err != nil {
t.Fatal(err)
}
payload, err := io.ReadAll(p)
if err != nil {
t.Fatal(err)
}
t.Run("header", func(t *testing.T) {
buf := &bytes.Buffer{}
if err := hdr.Marshal(buf); err != nil {
t.Fatal(err)
}
buf.Write(payload)
if !bytes.Equal(buf.Bytes(), v.file) {
t.Error("got a different header+payload encoding")
}
})
if v.expect == "success" {
t.Run("STREAM", func(t *testing.T) {
nonce, payload := payload[:16], payload[16:]
key := streamKey(v.fileKey[:], nonce)
r, err := stream.NewReader(key, bytes.NewReader(payload))
if err != nil {
t.Fatal(err)
}
plaintext, err := io.ReadAll(r)
if err != nil {
t.Fatal(err)
}
buf := &bytes.Buffer{}
w, err := stream.NewWriter(key, buf)
if err != nil {
t.Fatal(err)
}
if _, err := w.Write(plaintext); err != nil {
t.Fatal(err)
}
if err := w.Close(); err != nil {
t.Fatal(err)
}
if !bytes.Equal(buf.Bytes(), payload) {
t.Error("got a different STREAM ciphertext")
}
})
}
}
func streamKey(fileKey, nonce []byte) []byte {
h := hkdf.New(sha256.New, fileKey, nonce, []byte("payload"))
streamKey := make([]byte, chacha20poly1305.KeySize)
if _, err := io.ReadFull(h, streamKey); err != nil {
panic("age: internal error: failed to read from HKDF: " + err.Error())
}
return streamKey
}