mirror of
https://github.com/FiloSottile/age.git
synced 2026-11-08 12:26:58 +00:00
Compare commits
72
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6b5f4300f | ||
|
|
627e6bc9d8 | ||
|
|
7ed486868a | ||
|
|
2a761fcb8c | ||
|
|
98e7afcbac | ||
|
|
5ef63b6153 | ||
|
|
bc21ece498 | ||
|
|
69c21b83fb | ||
|
|
35cf02b1d0 | ||
|
|
29b68c20fc | ||
|
|
101cc86763 | ||
|
|
6ad4560f4a | ||
|
|
93055632ad | ||
|
|
294b0aa1e3 | ||
|
|
f1f96c25e0 | ||
|
|
9fd564d543 | ||
|
|
c89f0b932e | ||
|
|
dd733c5c0f | ||
|
|
004b544d83 | ||
|
|
02181d83e9 | ||
|
|
6976c5fca5 | ||
|
|
980763a16e | ||
|
|
4740a92ef9 | ||
|
|
6c36e167c8 | ||
|
|
9f0a2d25ac | ||
|
|
b6537b1865 | ||
|
|
486b6dac96 | ||
|
|
877ca247e3 | ||
|
|
502b180b17 | ||
|
|
8e3f74c283 | ||
|
|
edf7388f77 | ||
|
|
5471e05672 | ||
|
|
c6dcfa1efc | ||
|
|
a1fabee4c8 | ||
|
|
7354aa0d08 | ||
|
|
bf8d2a3911 | ||
|
|
5d5c9c48d8 | ||
|
|
e05ce267a6 | ||
|
|
000e931101 | ||
|
|
ebf48f1bbc | ||
|
|
3f2209cab9 | ||
|
|
bbac0a501c | ||
|
|
02a01c6a6f | ||
|
|
8328d19d3e | ||
|
|
1f84a45175 | ||
|
|
084c974f53 | ||
|
|
36ae5671cf | ||
|
|
8a02f4801f | ||
|
|
e41463e117 | ||
|
|
f66877cfa5 | ||
|
|
891be91d42 | ||
|
|
a5d38ae6ce | ||
|
|
799c2bf8e8 | ||
|
|
e84d74239e | ||
|
|
95ba0188d1 | ||
|
|
0ab5c738fb | ||
|
|
de7c1fb565 | ||
|
|
92fb4d508c | ||
|
|
331b242a9c | ||
|
|
c50f1ae2e1 | ||
|
|
8023f06ce2 | ||
|
|
4f7bb44386 | ||
|
|
3f56ac13fb | ||
|
|
2e090545df | ||
|
|
2088adf268 | ||
|
|
eaa4e03cfe | ||
|
|
f8a121dd87 | ||
|
|
bb4493a7cd | ||
|
|
92713afd1e | ||
|
|
787044bdb6 | ||
|
|
a787511e01 | ||
|
|
e49b1f9afe |
No files matched your search
@@ -1 +1,2 @@
|
||||
*.age binary
|
||||
testdata/testkit/* binary
|
||||
@@ -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
@@ -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 }}
|
||||
@@ -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 }}
|
||||
@@ -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:
|
||||
|
||||
@@ -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"]
|
||||
@@ -1,5 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
source "https://rubygems.org"
|
||||
|
||||
gem "ronn", "~> 0.7.3"
|
||||
@@ -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
|
||||
@@ -1,4 +0,0 @@
|
||||
name: Ronn
|
||||
runs:
|
||||
using: docker
|
||||
image: Dockerfile
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
[](https://pkg.go.dev/filippo.io/age)
|
||||
[-man%20page-lightgrey)](https://filippo.io/age/age.1)
|
||||
[-man%20page-lightgrey>)](https://filippo.io/age/age.1)
|
||||
[](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.
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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"
|
||||
)
|
||||
|
||||
Vendored
-6
@@ -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
@@ -1 +0,0 @@
|
||||
now-major-idea-author-clerk-bronze-all-soul-uncover-glad
|
||||
Vendored
+32
@@ -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
|
||||
Vendored
+125
@@ -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=
|
||||
Vendored
-5
@@ -1,5 +0,0 @@
|
||||
age-encryption.org/v1
|
||||
-> X25519 i6JOY3uvMdBuEybYbTp3ECFsOPEY/A3lJY1l0Qv2NC4
|
||||
cD7VpfIOchU6ZjAccEjlPCNSOdJvVkxZPSf+7XS1YhY
|
||||
--- 1111111111111111111111111111111111111111111
|
||||
�-\�P9��0�hń��Tt�|:٘�#&R�r� ��
|
||||
@@ -1,7 +0,0 @@
|
||||
age-encryption.org/v1
|
||||
-> scrypt z8U9dYMQuK1fFdvtpQYLEQ 10
|
||||
5SVjw1bbFCZLdI1FR7RqfTd3yWo4KS1ikOjvz60Bpqhrv0W6o6/2oszxZEm1gEUC
|
||||
|
||||
--- YXxSwONGPBbV7woMuEFTYuA03qTYUF1k0Y8j/NDEu1o
|
||||
’Ì
|
||||
Å!í‡ i�Þ.f¦æš{ŸdËIE]© ñn”$È!b®2
|
||||
@@ -1,6 +0,0 @@
|
||||
age-encryption.org/v1
|
||||
-> X25519 UkSgrxSETNpdkHY8EwiiRivqks2QJLUzsNsVjUTDcmw
|
||||
8yB9TqsBo4Ypchw07AtemV5TW4sGwyPDPMIfRg8Ve8rbDXt4tCwnnKcMq2K6aoqx
|
||||
|
||||
--- vUhLU0U9Dc8YhbKy4SxKuq0iSqqjBWGnHfZG+9+O4v4
|
||||
タケ��イhエW諾餞I暠fッニ�索Q;ミRh皷w
|
||||
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -1,5 +0,0 @@
|
||||
age-encryption.org/v1
|
||||
-> scrypt 1Q6WlGmsRulbN7bmUw8A1Q 23
|
||||
GP2lnzFuk1dgEkcMPmK6KkmuOm5gIWJzLeuwGcRsvAY
|
||||
--- vXvOsVbDbMc0x5Js1FS6k1ViOJ3H2ZdSUZo9bfvbzmU
|
||||
=ñÅÙœ£¨v>¹vhKMû'ÕßúÙ½NeçS†Äñ\(_ó
|
||||
Vendored
-5
@@ -1,5 +0,0 @@
|
||||
age-encryption.org/v1
|
||||
-> ssh-ed25519 cp09gQ Kf5JDNFFDUaOvups2MDfP47PlrpJthnmz0WNMfGj+C8
|
||||
hQ76lMAsG2pjR8GHTU+XU0giePyzE3prVmAw5MbMxSk
|
||||
--- CjO8qf3vd83otqHSflgWP5gQoe2Roo9tf/zgWEy9t0U
|
||||
èÙ®Ò%JðÇnj<bÖ: %UgZ4#™±àNÂ韟
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
||||
QyNTUxOQAAACB/aTuac9tiWRGrKEtixFlryYlGCPTOpdbmXN9RRmDF2gAAAKDgV/GC4Ffx
|
||||
ggAAAAtzc2gtZWQyNTUxOQAAACB/aTuac9tiWRGrKEtixFlryYlGCPTOpdbmXN9RRmDF2g
|
||||
AAAECvFoQXQzXgJLQ+Gz4PfEcfyZwC2gUjOiWTD//mTPyD8H9pO5pz22JZEasoS2LEWWvJ
|
||||
iUYI9M6l1uZc31FGYMXaAAAAG2ZpbGlwcG9AQmlzdHJvbWF0aC1NMS5sb2NhbAEC
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
-1
@@ -1 +0,0 @@
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH9pO5pz22JZEasoS2LEWWvJiUYI9M6l1uZc31FGYMXa
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
age-encryption.org/v1
|
||||
-> X25519 JRosIz2avWchP2qSL6wF6U7uzD6kDuJXDbZvN1MOGmo
|
||||
KpIQxpkbBDHqp+JsHLiTy2d5RYRwp2qzvUrAe0aDOnk
|
||||
--- orVjbqbzm8U3S9njAs53o4PFi1wK39fIQQ4gRj3i7IU
|
||||
„Ïgñ¾�Ô0‡‘µÆN¢'jûöao�¹&æT
|
||||
@@ -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
|
||||
Vendored
BIN
Binary file not shown.
Vendored
-13
@@ -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
@@ -1 +0,0 @@
|
||||
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDULTit0KUehbigbhsQaZo2oXLgMug8nTfnzDhcf5cEzdOJZyF6i5aRQbqbXIOYeTS3Shpp/iE6d60qi93JBBfveAZqr76tK7lVK8fstvAAGgbo9L9Ru5nhWX0HTlZUOodUT2E0rgAeoFzfvcZTaajzPmikrESmuaJLcdJ3crbCIyovkTxc85KbSn+Ky5grntdVR7GXve0HuIgwuSXGNyO0/hEQyhCEco4w53LZDXl3Dlxgb52u0QCsFFiQJmZ3RWnbCz2mNjM9Vo5KRt2pJ4u74lVsfXCvAqG4GsDd5WXCNRNaQ7gUdw1eSnvvcy3hxeEqLNN9tHaFCjEuyPfXfTVd3t0SlXreyoNbV75rYvZ2F6aLxyMMys+5bpnX0tu2LSq4Bvj5xIsBokjgEd3UXyj1eErsQdUJ0gghAMpXOjVbISwPAGMvUsH2unGbjvfaxnK57S7KAXOYznjhBf5ayIleAc+4LVD/KeMtGbpkyXXozC9jwH9cZBbGU4BtElAbzh8=
|
||||
@@ -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ÒÛ¨
|
||||
Vendored
-6
@@ -1,6 +0,0 @@
|
||||
age-encryption.org/v1
|
||||
-> X25519 kx2RzHNfNuts0I131KwMCyYclZzKCGMzPUaMkH9J4z4
|
||||
9qEzjtIF4NsLFnxv8EEtCwOQiXj5WHl+HWaDKNeAk+4
|
||||
--- N+7l3M/ofCyzZVlPJ33CTHH8AddF0itK70QV+IIvXXA
|
||||
³]Ú É+zAIÉúçê¸Ç�éLüü“ªžà
|
||||
Hˆ%Ñ¥£
|
||||
Vendored
-5
@@ -1,5 +0,0 @@
|
||||
age-encryption.org/v1
|
||||
-> scrypt X6oOTRAjCR1xid0PlnNMFA 10
|
||||
hszKAHhyFVpUgt9niYpdYXVhhN+r+oiCLPZukDdQZBQ
|
||||
--- 7BRJPVjbIC1JntvHrA13PQrnsa3lkwhnNF/Pbo4BPs4
|
||||
4|)ÆS|Û‹Ò¿D�÷§}’Í2è%�e�=óœ6˜»ÇZ
|
||||
Vendored
-5
@@ -1,5 +0,0 @@
|
||||
age-encryption.org/v1
|
||||
-> X25519 Rp86RQ3LgUJpQy4X2RMUhURlBP28tCaLQ2ssysJfRhg
|
||||
83YXad/lj3/wFM4n7vlGIiBSgfhG8lfiP5U7ajjK3HM
|
||||
--- O2+UpzetsP2+7BPyGQ4C6VMTY6zwp5TiNpVcFy4qdyM
|
||||
璇漡�(擔Q:|c蔑鞮蓤�=f�6b坿!
|
||||
Vendored
+54
@@ -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
|
||||
Vendored
+13
@@ -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-----
|
||||
Vendored
+20
@@ -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=
|
||||
Vendored
+59
@@ -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 --
|
||||
|
||||
Vendored
+56
@@ -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 --
|
||||
|
||||
Vendored
+20
@@ -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:'
|
||||
Vendored
+23
@@ -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
@@ -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) }
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
|
||||
@@ -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`:
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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=
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
File renamed without changes.
+274
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user