mirror of
https://github.com/FiloSottile/age.git
synced 2026-01-19 01:42:47 +00:00
Compare commits
34 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 |
2
.github/CONTRIBUTING.md
vendored
2
.github/CONTRIBUTING.md
vendored
@@ -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.
|
||||
|
||||
3
.github/workflows/build.yml
vendored
3
.github/workflows/build.yml
vendored
@@ -10,7 +10,6 @@ jobs:
|
||||
build:
|
||||
name: Build binaries
|
||||
runs-on: ubuntu-latest
|
||||
environment: "Build, sign, release binaries"
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
@@ -27,7 +26,7 @@ jobs:
|
||||
with:
|
||||
go-version: 1.x
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Build binary
|
||||
|
||||
11
.github/workflows/interop.yml
vendored
11
.github/workflows/interop.yml
vendored
@@ -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 }}
|
||||
|
||||
10
.github/workflows/ronn.yml
vendored
10
.github/workflows/ronn.yml
vendored
@@ -6,7 +6,6 @@ on:
|
||||
paths:
|
||||
- '**.ronn'
|
||||
- '**/ronn.yml'
|
||||
- '**/ronn/**'
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
@@ -15,10 +14,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
- name: Install ronn
|
||||
run: sudo apt-get update && sudo apt-get install -y ronn
|
||||
- name: Run ronn
|
||||
uses: ./.github/workflows/ronn
|
||||
id: ronn
|
||||
run: bash -O globstar -c 'ronn **/*.ronn'
|
||||
- name: Undo email mangling
|
||||
# rdiscount randomizes the output for no good reason, which causes
|
||||
# changes to always get committed. Sigh.
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
- name: Download generated files
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
|
||||
8
.github/workflows/ronn/Dockerfile
vendored
8
.github/workflows/ronn/Dockerfile
vendored
@@ -1,8 +0,0 @@
|
||||
FROM ruby:3.0.1-buster
|
||||
|
||||
RUN apt-get update && apt-get install -y groff
|
||||
RUN bundle config --global frozen 1
|
||||
COPY Gemfile Gemfile.lock ./
|
||||
RUN bundle install
|
||||
ENTRYPOINT ["bash", "-O", "globstar", "-c", \
|
||||
"/usr/local/bundle/bin/ronn **/*.ronn"]
|
||||
5
.github/workflows/ronn/Gemfile
vendored
5
.github/workflows/ronn/Gemfile
vendored
@@ -1,5 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
source "https://rubygems.org"
|
||||
|
||||
gem "ronn", "~> 0.7.3"
|
||||
20
.github/workflows/ronn/Gemfile.lock
vendored
20
.github/workflows/ronn/Gemfile.lock
vendored
@@ -1,20 +0,0 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
hpricot (0.8.6)
|
||||
mustache (1.1.1)
|
||||
rdiscount (2.2.0.2)
|
||||
ronn (0.7.3)
|
||||
hpricot (>= 0.8.2)
|
||||
mustache (>= 0.7.0)
|
||||
rdiscount (>= 1.5.8)
|
||||
|
||||
PLATFORMS
|
||||
aarch64-linux
|
||||
x86_64-linux
|
||||
|
||||
DEPENDENCIES
|
||||
ronn (~> 0.7.3)
|
||||
|
||||
BUNDLED WITH
|
||||
2.2.15
|
||||
4
.github/workflows/ronn/action.yml
vendored
4
.github/workflows/ronn/action.yml
vendored
@@ -1,4 +0,0 @@
|
||||
name: Ronn
|
||||
runs:
|
||||
using: docker
|
||||
image: Dockerfile
|
||||
29
.github/workflows/test.yml
vendored
29
.github/workflows/test.yml
vendored
@@ -8,37 +8,20 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
go: [1.18.x, 1.19.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
|
||||
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
|
||||
|
||||
24
README.md
24
README.md
@@ -1,4 +1,10 @@
|
||||
<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)
|
||||
@@ -23,7 +29,9 @@ $ age --decrypt -i key.txt data.tar.gz.age > data.tar.gz
|
||||
|
||||
🔑 Hardware PIV tokens such as YubiKeys are supported through the [age-plugin-yubikey](https://github.com/str4d/age-plugin-yubikey) plugin.
|
||||
|
||||
💬 The author pronounces it `[aɡe̞]`, like the Italian [“aghe”](https://translate.google.com/?sl=it&text=aghe).
|
||||
✨ 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
|
||||
|
||||
@@ -53,7 +61,13 @@ $ age --decrypt -i key.txt data.tar.gz.age > data.tar.gz
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Debian 11+ (Bullseye)</td>
|
||||
<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+)
|
||||
@@ -116,7 +130,7 @@ $ age --decrypt -i key.txt data.tar.gz.age > data.tar.gz
|
||||
<tr>
|
||||
<td>Scoop (Windows)</td>
|
||||
<td>
|
||||
<code>scoop bucket add extras; scoop install age</code>
|
||||
<code>scoop bucket add extras && scoop install age</code>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -125,7 +139,7 @@ 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
|
||||
https://dl.filippo.io/age/v1.1.1?for=darwin/arm64
|
||||
...
|
||||
```
|
||||
|
||||
|
||||
59
age.go
59
age.go
@@ -13,7 +13,7 @@
|
||||
// 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,
|
||||
// textual, and cheap, you are encouraged to generate dedicated keys for each
|
||||
@@ -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 {
|
||||
|
||||
64
age_test.go
64
age_test.go
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -465,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)
|
||||
}
|
||||
|
||||
@@ -36,19 +36,27 @@ func TestMain(m *testing.M) {
|
||||
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()
|
||||
@@ -56,6 +64,8 @@ func TestMain(m *testing.M) {
|
||||
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:
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
18
cmd/age/testdata/encrypted_keys.txt
vendored
18
cmd/age/testdata/encrypted_keys.txt
vendored
@@ -1,19 +1,19 @@
|
||||
# TODO: age-encrypted private keys, multiple identities, -i ordering, -e -i,
|
||||
# age file password prompt during encryption
|
||||
|
||||
[windows] skip # no pty support
|
||||
[!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
|
||||
pty terminal
|
||||
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
|
||||
pty terminal
|
||||
ttyin terminal
|
||||
age -d -i key_ed25519 ed25519.age
|
||||
cmp stdout input
|
||||
|
||||
@@ -24,7 +24,7 @@ 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
|
||||
pty terminal
|
||||
ttyin terminal
|
||||
age -d -i key_rsa_legacy rsa_legacy.age
|
||||
cmp stdout input
|
||||
! stderr .
|
||||
@@ -34,7 +34,7 @@ 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
|
||||
pty terminal
|
||||
ttyin terminal
|
||||
age -d -i key_rsa_legacy rsa_legacy.age
|
||||
cmp stdout input
|
||||
|
||||
@@ -45,17 +45,17 @@ stderr 'key_rsa_legacy.pub'
|
||||
|
||||
# mismatched .pub file causes an error
|
||||
cp key_rsa_legacy key_rsa_other
|
||||
pty terminal
|
||||
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
|
||||
pty terminal
|
||||
ttyin terminal
|
||||
age -e -i key_ed25519 -a -o test.age input
|
||||
exec cat test.age terminal # concatenated ciphertext + password
|
||||
pty -stdin stdout
|
||||
ttyin -stdin stdout
|
||||
age -d -i key_ed25519
|
||||
ptyout 'Enter passphrase'
|
||||
ttyout 'Enter passphrase'
|
||||
! stderr .
|
||||
cmp stdout input
|
||||
|
||||
|
||||
54
cmd/age/testdata/output_file.txt
vendored
Normal file
54
cmd/age/testdata/output_file.txt
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
# https://github.com/FiloSottile/age/issues/57
|
||||
age -r age1xmwwc06ly3ee5rytxm9mflaz2u56jjj36s0mypdrwsvlul66mv4q47ryef -o test.age input
|
||||
! age -o test.out -d -i wrong.txt test.age
|
||||
! exists test.out
|
||||
! age -o test.out -d test.age
|
||||
! exists test.out
|
||||
! age -o test.out -d -i notexist test.age
|
||||
! exists test.out
|
||||
! age -o test.out -d -i wrong.txt notexist
|
||||
! exists test.out
|
||||
! age -o test.out -r BAD
|
||||
! exists test.out
|
||||
! age -o test.out -r age1xmwwc06ly3ee5rytxm9mflaz2u56jjj36s0mypdrwsvlul66mv4q47ryef notexist
|
||||
! exists test.out
|
||||
! age -o test.out -p notexist
|
||||
! exists test.out
|
||||
|
||||
# https://github.com/FiloSottile/age/issues/555
|
||||
age -r age1xmwwc06ly3ee5rytxm9mflaz2u56jjj36s0mypdrwsvlul66mv4q47ryef -o empty.age empty
|
||||
exists empty.age
|
||||
age -d -i key.txt empty.age
|
||||
! stdout .
|
||||
! stderr .
|
||||
age -d -i key.txt -o new empty.age
|
||||
! stderr .
|
||||
cmp new empty
|
||||
|
||||
[!linux] [!darwin] skip # no pty support
|
||||
|
||||
# https://github.com/FiloSottile/age/issues/159
|
||||
ttyin terminal
|
||||
age -p -a -o test.age input
|
||||
ttyin terminalwrong
|
||||
! age -o test.out -d test.age
|
||||
ttyout 'Enter passphrase'
|
||||
stderr 'incorrect passphrase'
|
||||
! exists test.out
|
||||
|
||||
-- terminal --
|
||||
password
|
||||
password
|
||||
-- terminalwrong --
|
||||
wrong
|
||||
-- input --
|
||||
age
|
||||
-- empty --
|
||||
-- key.txt --
|
||||
# created: 2021-02-02T13:09:43+01:00
|
||||
# public key: age1xmwwc06ly3ee5rytxm9mflaz2u56jjj36s0mypdrwsvlul66mv4q47ryef
|
||||
AGE-SECRET-KEY-1EGTZVFFV20835NWYV6270LXYVK2VKNX2MMDKWYKLMGR48UAWX40Q2P2LM0
|
||||
-- wrong.txt --
|
||||
# created: 2024-06-16T12:14:00+02:00
|
||||
# public key: age10k7vsqmeg3sp8elfyq5ts55feg4huarpcaf9dmljn9umydg3gymsvx4dp9
|
||||
AGE-SECRET-KEY-1NPX08S4LELW9K68FKU0U05XXEKG6X7GT004TPNYLF86H3M00D3FQ3VQQNN
|
||||
20
cmd/age/testdata/scrypt.txt
vendored
20
cmd/age/testdata/scrypt.txt
vendored
@@ -1,42 +1,42 @@
|
||||
[windows] skip # no pty support
|
||||
[!linux] [!darwin] skip # no pty support
|
||||
|
||||
# encrypt with a provided passphrase
|
||||
stdin input
|
||||
pty terminal
|
||||
ttyin terminal
|
||||
age -p -o test.age
|
||||
ptyout 'Enter passphrase'
|
||||
ttyout 'Enter passphrase'
|
||||
! stderr .
|
||||
! stdout .
|
||||
|
||||
# decrypt with a provided passphrase
|
||||
pty terminal
|
||||
ttyin terminal
|
||||
age -d test.age
|
||||
ptyout 'Enter passphrase'
|
||||
ttyout 'Enter passphrase'
|
||||
! stderr .
|
||||
cmp stdout input
|
||||
|
||||
# decrypt with the wrong passphrase
|
||||
pty wrong
|
||||
ttyin wrong
|
||||
! age -d test.age
|
||||
stderr 'incorrect passphrase'
|
||||
|
||||
# encrypt with a generated passphrase
|
||||
stdin input
|
||||
pty empty
|
||||
ttyin empty
|
||||
age -p -o test.age
|
||||
! stderr .
|
||||
! stdout .
|
||||
pty autogenerated
|
||||
ttyin autogenerated
|
||||
age -d test.age
|
||||
cmp stdout input
|
||||
|
||||
# fail when -i is present
|
||||
pty terminal
|
||||
ttyin terminal
|
||||
! age -d -i key.txt test.age
|
||||
stderr 'file is passphrase-encrypted but identities were specified'
|
||||
|
||||
# fail when passphrases don't match
|
||||
pty wrong
|
||||
ttyin wrong
|
||||
! age -p -o fail.age
|
||||
stderr 'passphrases didn''t match'
|
||||
! exists fail.age
|
||||
|
||||
22
cmd/age/testdata/terminal.txt
vendored
22
cmd/age/testdata/terminal.txt
vendored
@@ -1,21 +1,21 @@
|
||||
[windows] skip # no pty support
|
||||
[!linux] [!darwin] skip # no pty support
|
||||
|
||||
# controlling terminal is used instead of stdin/stderr
|
||||
pty terminal
|
||||
ttyin terminal
|
||||
age -p -o test.age input
|
||||
! stderr .
|
||||
|
||||
# autogenerated passphrase is printed to terminal
|
||||
pty empty
|
||||
ttyin empty
|
||||
age -p -o test.age input
|
||||
ptyout 'autogenerated passphrase'
|
||||
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
|
||||
# pty -stdin terminal
|
||||
# ttyin -stdin terminal
|
||||
# age -p -o test.age input
|
||||
# ! stderr .
|
||||
|
||||
@@ -28,22 +28,22 @@ ptyout 'autogenerated passphrase'
|
||||
|
||||
# prompt for password before plaintext if stdin is the terminal
|
||||
exec cat terminal input # concatenated password + input
|
||||
pty -stdin stdout
|
||||
ttyin -stdin stdout
|
||||
age -p -a -o test.age
|
||||
ptyout 'Enter passphrase'
|
||||
ttyout 'Enter passphrase'
|
||||
! stderr .
|
||||
# check the file was encrypted correctly
|
||||
pty terminal
|
||||
ttyin terminal
|
||||
age -d test.age
|
||||
cmp stdout input
|
||||
|
||||
# buffer armored ciphertext before prompting if stdin is the terminal
|
||||
pty terminal
|
||||
ttyin terminal
|
||||
age -p -a -o test.age input
|
||||
exec cat test.age terminal # concatenated ciphertext + password
|
||||
pty -stdin stdout
|
||||
ttyin -stdin stdout
|
||||
age -d
|
||||
ptyout 'Enter passphrase'
|
||||
ttyout 'Enter passphrase'
|
||||
! stderr .
|
||||
cmp stdout input
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ import (
|
||||
"runtime"
|
||||
|
||||
"filippo.io/age/armor"
|
||||
"filippo.io/age/internal/plugin"
|
||||
"filippo.io/age/plugin"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
|
||||
@@ -1,88 +1,56 @@
|
||||
.\" generated with Ronn/v0.7.3
|
||||
.\" http://github.com/rtomayko/ronn/tree/0.7.3
|
||||
.
|
||||
.TH "AGE\-KEYGEN" "1" "September 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
|
||||
|
||||
@@ -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'>September 2022</li>
|
||||
<li class='tc'>April 2023</li>
|
||||
<li class='tr'>age-keygen(1)</li>
|
||||
</ol>
|
||||
|
||||
|
||||
155
doc/age.1
155
doc/age.1
@@ -1,281 +1,184 @@
|
||||
.\" generated with Ronn/v0.7.3
|
||||
.\" http://github.com/rtomayko/ronn/tree/0.7.3
|
||||
.
|
||||
.TH "AGE" "1" "September 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 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
|
||||
|
||||
153
doc/age.1.html
153
doc/age.1.html
@@ -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
|
||||
@@ -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'>September 2022</li>
|
||||
<li class='tc'>April 2023</li>
|
||||
<li class='tr'>age(1)</li>
|
||||
</ol>
|
||||
|
||||
|
||||
18
go.mod
18
go.mod
@@ -1,19 +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 (
|
||||
github.com/creack/pty v1.1.18 // indirect
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e // indirect
|
||||
github.com/rogpeppe/go-internal v1.8.1
|
||||
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
|
||||
)
|
||||
|
||||
replace github.com/rogpeppe/go-internal => github.com/FiloSottile/go-internal v1.8.2-0.20220728122003-0ced171a3e0e
|
||||
|
||||
34
go.sum
34
go.sum
@@ -1,20 +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=
|
||||
github.com/FiloSottile/go-internal v1.8.2-0.20220728122003-0ced171a3e0e h1:hLDldUUKSNgXte+2H8yZzPVStWa7697IJer9wwfW/dg=
|
||||
github.com/FiloSottile/go-internal v1.8.2-0.20220728122003-0ced171a3e0e/go.mod h1:dNbK7mWDMlmf5ttOAJJg+a4CyamnqDRrw+Uja1sBETc=
|
||||
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
|
||||
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
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=
|
||||
|
||||
@@ -12,15 +12,15 @@ import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
exec "golang.org/x/sys/execabs"
|
||||
|
||||
"filippo.io/age"
|
||||
"filippo.io/age/internal/bech32"
|
||||
"filippo.io/age/internal/format"
|
||||
)
|
||||
|
||||
@@ -34,16 +34,13 @@ type Recipient struct {
|
||||
}
|
||||
|
||||
var _ age.Recipient = &Recipient{}
|
||||
var _ age.RecipientWithLabels = &Recipient{}
|
||||
|
||||
func NewRecipient(s string, ui *ClientUI) (*Recipient, error) {
|
||||
hrp, _, err := bech32.Decode(s)
|
||||
name, _, err := ParseRecipient(s)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid recipient encoding %q: %v", s, err)
|
||||
return nil, err
|
||||
}
|
||||
if !strings.HasPrefix(hrp, "age1") {
|
||||
return nil, fmt.Errorf("not a plugin recipient %q: %v", s, err)
|
||||
}
|
||||
name := strings.TrimPrefix(hrp, "age1")
|
||||
return &Recipient{
|
||||
name: name, encoding: s, ui: ui,
|
||||
}, nil
|
||||
@@ -57,6 +54,11 @@ func (r *Recipient) Name() string {
|
||||
}
|
||||
|
||||
func (r *Recipient) Wrap(fileKey []byte) (stanzas []*age.Stanza, err error) {
|
||||
stanzas, _, err = r.WrapWithLabels(fileKey)
|
||||
return
|
||||
}
|
||||
|
||||
func (r *Recipient) WrapWithLabels(fileKey []byte) (stanzas []*age.Stanza, labels []string, err error) {
|
||||
defer func() {
|
||||
if err != nil {
|
||||
err = fmt.Errorf("%s plugin: %w", r.name, err)
|
||||
@@ -65,7 +67,7 @@ func (r *Recipient) Wrap(fileKey []byte) (stanzas []*age.Stanza, err error) {
|
||||
|
||||
conn, err := openClientConnection(r.name, "recipient-v1")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("couldn't start plugin: %v", err)
|
||||
return nil, nil, fmt.Errorf("couldn't start plugin: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
@@ -75,13 +77,19 @@ func (r *Recipient) Wrap(fileKey []byte) (stanzas []*age.Stanza, err error) {
|
||||
addType = "add-identity"
|
||||
}
|
||||
if err := writeStanza(conn, addType, r.encoding); err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
if err := writeStanza(conn, fmt.Sprintf("grease-%x", rand.Int())); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if err := writeStanzaWithBody(conn, "wrap-file-key", fileKey); err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
if err := writeStanza(conn, "extension-labels"); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if err := writeStanza(conn, "done"); err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Phase 2: plugin responds with stanzas
|
||||
@@ -90,21 +98,21 @@ ReadLoop:
|
||||
for {
|
||||
s, err := r.ui.readStanza(r.name, sr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
switch s.Type {
|
||||
case "recipient-stanza":
|
||||
if len(s.Args) < 2 {
|
||||
return nil, fmt.Errorf("malformed recipient stanza: unexpected argument count")
|
||||
return nil, nil, fmt.Errorf("malformed recipient stanza: unexpected argument count")
|
||||
}
|
||||
n, err := strconv.Atoi(s.Args[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("malformed recipient stanza: invalid index")
|
||||
return nil, nil, fmt.Errorf("malformed recipient stanza: invalid index")
|
||||
}
|
||||
// We only send a single file key, so the index must be 0.
|
||||
if n != 0 {
|
||||
return nil, fmt.Errorf("malformed recipient stanza: unexpected index")
|
||||
return nil, nil, fmt.Errorf("malformed recipient stanza: unexpected index")
|
||||
}
|
||||
|
||||
stanzas = append(stanzas, &age.Stanza{
|
||||
@@ -114,32 +122,41 @@ ReadLoop:
|
||||
})
|
||||
|
||||
if err := writeStanza(conn, "ok"); err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
case "labels":
|
||||
if labels != nil {
|
||||
return nil, nil, fmt.Errorf("repeated labels stanza")
|
||||
}
|
||||
labels = s.Args
|
||||
|
||||
if err := writeStanza(conn, "ok"); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
case "error":
|
||||
if err := writeStanza(conn, "ok"); err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("%s", s.Body)
|
||||
return nil, nil, fmt.Errorf("%s", s.Body)
|
||||
case "done":
|
||||
break ReadLoop
|
||||
default:
|
||||
if ok, err := r.ui.handle(r.name, conn, s); err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
} else if !ok {
|
||||
if err := writeStanza(conn, "unsupported"); err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(stanzas) == 0 {
|
||||
return nil, fmt.Errorf("received zero recipient stanzas")
|
||||
return nil, nil, fmt.Errorf("received zero recipient stanzas")
|
||||
}
|
||||
|
||||
return stanzas, nil
|
||||
return stanzas, labels, nil
|
||||
}
|
||||
|
||||
type Identity struct {
|
||||
@@ -151,25 +168,17 @@ type Identity struct {
|
||||
var _ age.Identity = &Identity{}
|
||||
|
||||
func NewIdentity(s string, ui *ClientUI) (*Identity, error) {
|
||||
hrp, _, err := bech32.Decode(s)
|
||||
name, _, err := ParseIdentity(s)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid identity encoding: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
if !strings.HasPrefix(hrp, "AGE-PLUGIN-") || !strings.HasSuffix(hrp, "-") {
|
||||
return nil, fmt.Errorf("not a plugin identity: %v", err)
|
||||
}
|
||||
name := strings.TrimSuffix(strings.TrimPrefix(hrp, "AGE-PLUGIN-"), "-")
|
||||
name = strings.ToLower(name)
|
||||
return &Identity{
|
||||
name: name, encoding: s, ui: ui,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewIdentityWithoutData(name string, ui *ClientUI) (*Identity, error) {
|
||||
s, err := bech32.Encode("AGE-PLUGIN-"+strings.ToUpper(name)+"-", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s := EncodeIdentity(name, nil)
|
||||
return &Identity{
|
||||
name: name, encoding: s, ui: ui,
|
||||
}, nil
|
||||
@@ -211,6 +220,9 @@ func (i *Identity) Unwrap(stanzas []*age.Stanza) (fileKey []byte, err error) {
|
||||
if err := writeStanza(conn, "add-identity", i.encoding); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := writeStanza(conn, fmt.Sprintf("grease-%x", rand.Int())); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, rs := range stanzas {
|
||||
s := &format.Stanza{
|
||||
Type: "recipient-stanza",
|
||||
@@ -374,8 +386,14 @@ type clientConnection struct {
|
||||
close func()
|
||||
}
|
||||
|
||||
var testOnlyPluginPath string
|
||||
|
||||
func openClientConnection(name, protocol string) (*clientConnection, error) {
|
||||
cmd := exec.Command("age-plugin-"+name, "--age-plugin="+protocol)
|
||||
path := "age-plugin-" + name
|
||||
if testOnlyPluginPath != "" {
|
||||
path = filepath.Join(testOnlyPluginPath, path)
|
||||
}
|
||||
cmd := exec.Command(path, "--age-plugin="+protocol)
|
||||
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
133
plugin/client_test.go
Normal file
133
plugin/client_test.go
Normal file
@@ -0,0 +1,133 @@
|
||||
// Copyright 2023 The age Authors
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://developers.google.com/open-source/licenses/bsd
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"filippo.io/age"
|
||||
"filippo.io/age/internal/bech32"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
switch filepath.Base(os.Args[0]) {
|
||||
// TODO: deduplicate from cmd/age TestMain.
|
||||
case "age-plugin-test":
|
||||
switch os.Args[1] {
|
||||
case "--age-plugin=recipient-v1":
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
scanner.Scan() // add-recipient
|
||||
scanner.Scan() // body
|
||||
scanner.Scan() // grease
|
||||
scanner.Scan() // body
|
||||
scanner.Scan() // wrap-file-key
|
||||
scanner.Scan() // body
|
||||
fileKey := scanner.Text()
|
||||
scanner.Scan() // extension-labels
|
||||
scanner.Scan() // body
|
||||
scanner.Scan() // done
|
||||
scanner.Scan() // body
|
||||
os.Stdout.WriteString("-> recipient-stanza 0 test\n")
|
||||
os.Stdout.WriteString(fileKey + "\n")
|
||||
scanner.Scan() // ok
|
||||
scanner.Scan() // body
|
||||
os.Stdout.WriteString("-> done\n\n")
|
||||
os.Exit(0)
|
||||
default:
|
||||
panic(os.Args[1])
|
||||
}
|
||||
case "age-plugin-testpqc":
|
||||
switch os.Args[1] {
|
||||
case "--age-plugin=recipient-v1":
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
scanner.Scan() // add-recipient
|
||||
scanner.Scan() // body
|
||||
scanner.Scan() // grease
|
||||
scanner.Scan() // body
|
||||
scanner.Scan() // wrap-file-key
|
||||
scanner.Scan() // body
|
||||
fileKey := scanner.Text()
|
||||
scanner.Scan() // extension-labels
|
||||
scanner.Scan() // body
|
||||
scanner.Scan() // done
|
||||
scanner.Scan() // body
|
||||
os.Stdout.WriteString("-> recipient-stanza 0 test\n")
|
||||
os.Stdout.WriteString(fileKey + "\n")
|
||||
scanner.Scan() // ok
|
||||
scanner.Scan() // body
|
||||
os.Stdout.WriteString("-> labels postquantum\n\n")
|
||||
scanner.Scan() // ok
|
||||
scanner.Scan() // body
|
||||
os.Stdout.WriteString("-> done\n\n")
|
||||
os.Exit(0)
|
||||
default:
|
||||
panic(os.Args[1])
|
||||
}
|
||||
default:
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLabels(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("Windows support is TODO")
|
||||
}
|
||||
temp := t.TempDir()
|
||||
testOnlyPluginPath = temp
|
||||
t.Cleanup(func() { testOnlyPluginPath = "" })
|
||||
ex, err := os.Executable()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Link(ex, filepath.Join(temp, "age-plugin-test")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Chmod(filepath.Join(temp, "age-plugin-test"), 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Link(ex, filepath.Join(temp, "age-plugin-testpqc")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Chmod(filepath.Join(temp, "age-plugin-testpqc"), 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
name, err := bech32.Encode("age1test", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testPlugin, err := NewRecipient(name, &ClientUI{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
namePQC, err := bech32.Encode("age1testpqc", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testPluginPQC, err := NewRecipient(namePQC, &ClientUI{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := age.Encrypt(io.Discard, testPluginPQC); err != nil {
|
||||
t.Errorf("expected one pqc to work, got %v", err)
|
||||
}
|
||||
if _, err := age.Encrypt(io.Discard, testPluginPQC, testPluginPQC); err != nil {
|
||||
t.Errorf("expected two pqc to work, got %v", err)
|
||||
}
|
||||
if _, err := age.Encrypt(io.Discard, testPluginPQC, testPlugin); err == nil {
|
||||
t.Errorf("expected one pqc and one normal to fail")
|
||||
}
|
||||
if _, err := age.Encrypt(io.Discard, testPlugin, testPluginPQC); err == nil {
|
||||
t.Errorf("expected one pqc and one normal to fail")
|
||||
}
|
||||
}
|
||||
55
plugin/encode.go
Normal file
55
plugin/encode.go
Normal file
@@ -0,0 +1,55 @@
|
||||
// Copyright 2023 The age Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"filippo.io/age/internal/bech32"
|
||||
)
|
||||
|
||||
// EncodeIdentity encodes a plugin identity string for a plugin with the given
|
||||
// name. If the name is invalid, it returns an empty string.
|
||||
func EncodeIdentity(name string, data []byte) string {
|
||||
s, _ := bech32.Encode("AGE-PLUGIN-"+strings.ToUpper(name)+"-", data)
|
||||
return s
|
||||
}
|
||||
|
||||
// ParseIdentity decodes a plugin identity string. It returns the plugin name
|
||||
// in lowercase and the encoded data.
|
||||
func ParseIdentity(s string) (name string, data []byte, err error) {
|
||||
hrp, data, err := bech32.Decode(s)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("invalid identity encoding: %v", err)
|
||||
}
|
||||
if !strings.HasPrefix(hrp, "AGE-PLUGIN-") || !strings.HasSuffix(hrp, "-") {
|
||||
return "", nil, fmt.Errorf("not a plugin identity: %v", err)
|
||||
}
|
||||
name = strings.TrimSuffix(strings.TrimPrefix(hrp, "AGE-PLUGIN-"), "-")
|
||||
name = strings.ToLower(name)
|
||||
return name, data, nil
|
||||
}
|
||||
|
||||
// EncodeRecipient encodes a plugin recipient string for a plugin with the given
|
||||
// name. If the name is invalid, it returns an empty string.
|
||||
func EncodeRecipient(name string, data []byte) string {
|
||||
s, _ := bech32.Encode("age1"+strings.ToLower(name), data)
|
||||
return s
|
||||
}
|
||||
|
||||
// ParseRecipient decodes a plugin recipient string. It returns the plugin name
|
||||
// in lowercase and the encoded data.
|
||||
func ParseRecipient(s string) (name string, data []byte, err error) {
|
||||
hrp, data, err := bech32.Decode(s)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("invalid recipient encoding: %v", err)
|
||||
}
|
||||
if !strings.HasPrefix(hrp, "age1") {
|
||||
return "", nil, fmt.Errorf("not a plugin recipient: %v", err)
|
||||
}
|
||||
name = strings.TrimPrefix(hrp, "age1")
|
||||
return name, data, nil
|
||||
}
|
||||
24
plugin/encode_go1.20.go
Normal file
24
plugin/encode_go1.20.go
Normal file
@@ -0,0 +1,24 @@
|
||||
// Copyright 2023 The age Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.20
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"crypto/ecdh"
|
||||
"fmt"
|
||||
|
||||
"filippo.io/age/internal/bech32"
|
||||
)
|
||||
|
||||
// EncodeX25519Recipient encodes a native X25519 recipient from a
|
||||
// [crypto/ecdh.X25519] public key. It's meant for plugins that implement
|
||||
// identities that are compatible with native recipients.
|
||||
func EncodeX25519Recipient(pk *ecdh.PublicKey) (string, error) {
|
||||
if pk.Curve() != ecdh.X25519() {
|
||||
return "", fmt.Errorf("wrong ecdh Curve")
|
||||
}
|
||||
return bech32.Encode("age", pk.Bytes())
|
||||
}
|
||||
24
scrypt.go
24
scrypt.go
@@ -6,6 +6,7 @@ package age
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
@@ -87,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
|
||||
|
||||
229
testkit_test.go
229
testkit_test.go
@@ -11,70 +11,57 @@ import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"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 TestVectors(t *testing.T) {
|
||||
if _, err := exec.LookPath("go"); err != nil {
|
||||
t.Skipf("skipping test because 'go' command is unavailable: %v", err)
|
||||
}
|
||||
|
||||
// Download the testkit files from CCTV using `go mod download -json` so the
|
||||
// cached source of the testdata can be reused.
|
||||
path := "c2sp.org/CCTV/age@v0.0.0-20221027185432-cfaa74dc42af"
|
||||
cmd := exec.Command("go", "mod", "download", "-json", path)
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to run `go mod download -json %s`, output: %s", path, output)
|
||||
}
|
||||
var dm struct {
|
||||
Dir string // absolute path to cached source root directory
|
||||
}
|
||||
if err := json.Unmarshal(output, &dm); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testkitDir := filepath.Join(dm.Dir, "testdata")
|
||||
|
||||
tests, err := filepath.Glob(testkitDir + "/*")
|
||||
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 {
|
||||
contents, err := os.ReadFile(test)
|
||||
name := test.Name()
|
||||
contents, err := fs.ReadFile(agetest.Vectors, name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
name := filepath.Base(test)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
testVector(t, contents)
|
||||
t.Parallel()
|
||||
f(t, parseVector(t, contents))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testVector(t *testing.T, test []byte) {
|
||||
var (
|
||||
expect string
|
||||
payloadHash *[32]byte
|
||||
identities []age.Identity
|
||||
armored bool
|
||||
)
|
||||
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(test, []byte("\n"))
|
||||
line, rest, ok := bytes.Cut(v.file, []byte("\n"))
|
||||
if !ok {
|
||||
t.Fatal("invalid test file: no payload")
|
||||
}
|
||||
test = rest
|
||||
v.file = rest
|
||||
if len(line) == 0 {
|
||||
break
|
||||
}
|
||||
@@ -91,87 +78,197 @@ func testVector(t *testing.T, test []byte) {
|
||||
default:
|
||||
t.Fatal("invalid test file: unknown expect value:", value)
|
||||
}
|
||||
expect = value
|
||||
v.expect = value
|
||||
case "payload":
|
||||
h, err := hex.DecodeString(value)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
payloadHash = (*[32]byte)(h)
|
||||
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)
|
||||
}
|
||||
identities = append(identities, i)
|
||||
v.identities = append(v.identities, i)
|
||||
case "passphrase":
|
||||
i, err := age.NewScryptIdentity(value)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
identities = append(identities, i)
|
||||
v.identities = append(v.identities, i)
|
||||
case "armored":
|
||||
armored = true
|
||||
case "file key":
|
||||
// Ignored.
|
||||
v.armored = true
|
||||
case "comment":
|
||||
t.Log(value)
|
||||
default:
|
||||
t.Fatal("invalid test file: unknown header key:", key)
|
||||
}
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
var in io.Reader = bytes.NewReader(test)
|
||||
if armored {
|
||||
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, identities...)
|
||||
r, err := age.Decrypt(in, v.identities...)
|
||||
if err != nil && strings.HasSuffix(err.Error(), "bad header MAC") {
|
||||
if expect == "HMAC failure" {
|
||||
if v.expect == "HMAC failure" {
|
||||
t.Log(err)
|
||||
return
|
||||
}
|
||||
t.Fatalf("expected %s, got HMAC error", expect)
|
||||
t.Fatalf("expected %s, got HMAC error", v.expect)
|
||||
} else if e := new(armor.Error); errors.As(err, &e) {
|
||||
if expect == "armor failure" {
|
||||
if v.expect == "armor failure" {
|
||||
t.Log(err)
|
||||
return
|
||||
}
|
||||
t.Fatalf("expected %s, got: %v", expect, err)
|
||||
t.Fatalf("expected %s, got: %v", v.expect, err)
|
||||
} else if _, ok := err.(*age.NoIdentityMatchError); ok {
|
||||
if expect == "no match" {
|
||||
if v.expect == "no match" {
|
||||
t.Log(err)
|
||||
return
|
||||
}
|
||||
t.Fatalf("expected %s, got: %v", expect, err)
|
||||
t.Fatalf("expected %s, got: %v", v.expect, err)
|
||||
} else if err != nil {
|
||||
if expect == "header failure" {
|
||||
if v.expect == "header failure" {
|
||||
t.Log(err)
|
||||
return
|
||||
}
|
||||
t.Fatalf("expected %s, got: %v", expect, err)
|
||||
} else if expect != "success" && expect != "payload failure" &&
|
||||
expect != "armor failure" {
|
||||
t.Fatalf("expected %s, got success", expect)
|
||||
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 && expect == "success" {
|
||||
t.Fatalf("expected %s, got: %v", expect, err)
|
||||
if err != nil && v.expect == "success" {
|
||||
t.Fatalf("expected %s, got: %v", v.expect, err)
|
||||
} else if err != nil {
|
||||
t.Log(err)
|
||||
if expect == "armor failure" {
|
||||
if v.expect == "armor failure" {
|
||||
if e := new(armor.Error); !errors.As(err, &e) {
|
||||
t.Errorf("expected armor.Error, got %T", err)
|
||||
}
|
||||
}
|
||||
if payloadHash != nil && sha256.Sum256(out) != *payloadHash {
|
||||
if v.payloadHash != nil && sha256.Sum256(out) != *v.payloadHash {
|
||||
t.Error("partial payload hash mismatch")
|
||||
}
|
||||
return
|
||||
} else if expect != "success" {
|
||||
t.Fatalf("expected %s, got success", expect)
|
||||
} else if v.expect != "success" {
|
||||
t.Fatalf("expected %s, got success", v.expect)
|
||||
}
|
||||
if sha256.Sum256(out) != *payloadHash {
|
||||
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