mirror of
https://github.com/FiloSottile/age.git
synced 2026-01-23 19:52:02 +00:00
Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
552aa0a07d | ||
|
|
47d8133c52 | ||
|
|
36b0a4f611 | ||
|
|
fda89073cd | ||
|
|
7756fbfe45 | ||
|
|
57f6b8acae | ||
|
|
e08055f4e5 | ||
|
|
7cb6b84758 | ||
|
|
4ea591b25f | ||
|
|
9d4b2ae7ac | ||
|
|
1ddf01df2c | ||
|
|
f4e28fe809 | ||
|
|
0703f86521 | ||
|
|
fb97277f8d | ||
|
|
fa5b575ceb | ||
|
|
cde103daae | ||
|
|
b403e96be8 | ||
|
|
329a7ece8f | ||
|
|
3cd503dce9 | ||
|
|
cd28d56599 | ||
|
|
a94f3c3dc9 | ||
|
|
6596145a2c | ||
|
|
7a262e1ffd | ||
|
|
0b895a9340 | ||
|
|
c9aca162ef | ||
|
|
c7c3012437 | ||
|
|
e58a8859b9 | ||
|
|
fb293ef526 | ||
|
|
3d5b49a348 | ||
|
|
cd4b2476bc | ||
|
|
759a88d3e8 | ||
|
|
85763d390a | ||
|
|
fff82986fa | ||
|
|
67ce088a41 | ||
|
|
3ad0bbed99 | ||
|
|
7a55783693 | ||
|
|
d271e916cf | ||
|
|
50254ff522 | ||
|
|
7a335c9d5d | ||
|
|
ff1b4ffb08 | ||
|
|
e63c22e327 | ||
|
|
a6a173e24f | ||
|
|
b4e0d7718f | ||
|
|
9e65644c3f | ||
|
|
290a2fd5ec | ||
|
|
bad2c0d2e0 | ||
|
|
dabc470bfe |
4
.github/ISSUE_TEMPLATE/bug-report.md
vendored
4
.github/ISSUE_TEMPLATE/bug-report.md
vendored
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report about a bug in this implementation.
|
||||
name: Bug report 🐞
|
||||
about: Did you encounter a bug in this implementation?
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
10
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
10
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
contact_links:
|
||||
- name: UX report ✨
|
||||
url: https://github.com/FiloSottile/age/discussions/new?category=UX-reports
|
||||
about: Was age hard to use? It's not you, it's us. We want to hear about it.
|
||||
- name: Spec feedback 📃
|
||||
url: https://github.com/FiloSottile/age/discussions/new?category=Spec-feedback
|
||||
about: Have a comment about the age spec as it's implemented by this and other tools?
|
||||
- name: Questions, feature requests, and more 💬
|
||||
url: https://github.com/FiloSottile/age/discussions
|
||||
about: Do you need support? Did you make something with age? Do you have an idea? Tell us about it!
|
||||
15
.github/ISSUE_TEMPLATE/spec-feedback.md
vendored
15
.github/ISSUE_TEMPLATE/spec-feedback.md
vendored
@@ -1,15 +0,0 @@
|
||||
---
|
||||
name: Spec feedback
|
||||
about: Have a comment about the age spec as it's implemented by this and other tools?
|
||||
title: 'spec: '
|
||||
labels: 'spec'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!-- This is the issue tracker of a specific implementation of
|
||||
the age format, which is specified at https://age-encryption.org/v1
|
||||
|
||||
Please consider using the mailing list to discuss the specification:
|
||||
|
||||
https://age-encryption.org/ml -->
|
||||
21
.github/ISSUE_TEMPLATE/ux-report.md
vendored
21
.github/ISSUE_TEMPLATE/ux-report.md
vendored
@@ -1,21 +0,0 @@
|
||||
---
|
||||
name: UX report
|
||||
about: Was age hard to use? It's not you, it's us. We want to hear about it.
|
||||
title: 'UX: '
|
||||
labels: 'UX report'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!-- Did age not do what you expected?
|
||||
Was it hard to figure out how to do something?
|
||||
Could an error message be more helpful?
|
||||
It's not you, it's us. We want to hear about it. -->
|
||||
|
||||
## What were you trying to do
|
||||
|
||||
## What happened
|
||||
|
||||
```
|
||||
<insert terminal transcript here>
|
||||
```
|
||||
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -39,7 +39,9 @@ jobs:
|
||||
GOOS=linux GOARCH=arm GOARM=6 build_age
|
||||
GOOS=linux GOARCH=arm64 build_age
|
||||
GOOS=darwin GOARCH=amd64 build_age
|
||||
GOOS=darwin GOARCH=arm64 build_age
|
||||
GOOS=windows GOARCH=amd64 build_age
|
||||
GOOS=freebsd GOARCH=amd64 build_age
|
||||
- name: Upload workflow artifacts
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
|
||||
2
.github/workflows/gotip.yml
vendored
2
.github/workflows/gotip.yml
vendored
@@ -15,14 +15,12 @@ jobs:
|
||||
git clone --filter=tree:0 https://go.googlesource.com/go $HOME/gotip
|
||||
cd $HOME/gotip/src && ./make.bash
|
||||
echo "$HOME/gotip/bin" >> $GITHUB_PATH
|
||||
echo "GOROOT=" >> $GITHUB_ENV # workaround actions/virtual-environments#2655
|
||||
- name: Install Go tip (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
run: |
|
||||
git clone --filter=tree:0 https://go.googlesource.com/go $HOME/gotip
|
||||
cd $HOME/gotip/src && ./make.bat
|
||||
echo "$HOME/gotip/bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
|
||||
echo "GOROOT=" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
|
||||
33
.github/workflows/ronn.yml
vendored
Normal file
33
.github/workflows/ronn.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
paths:
|
||||
- '**.ronn'
|
||||
name: Generate man pages
|
||||
jobs:
|
||||
ronn:
|
||||
runs-on: ubuntu-latest
|
||||
name: Ronn
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Run ronn
|
||||
uses: ./.github/workflows/ronn
|
||||
id: ronn
|
||||
- name: Undo email mangling
|
||||
# rdiscount randomizes the output for no good reason, which causes
|
||||
# changes to always get committed. Sigh.
|
||||
# https://github.com/davidfstr/rdiscount/blob/6b1471ec3/ext/generate.c#L781-L795
|
||||
run: |-
|
||||
for f in doc/*.html; do
|
||||
awk '/Filippo Valsorda/ { $0 = "<p>Filippo Valsorda <a href=\"mailto:age@filippo.io\" data-bare-link=\"true\">age@filippo.io</a></p>" } { print }' "$f" > "$f.tmp"
|
||||
mv "$f.tmp" "$f"
|
||||
done
|
||||
- name: Commit and push if changed
|
||||
run: |-
|
||||
git config user.name "GitHub Actions"
|
||||
git config user.email "actions@users.noreply.github.com"
|
||||
git add -A
|
||||
git commit -m "doc: regenerate groff and html man pages" || exit 0
|
||||
git push
|
||||
8
.github/workflows/ronn/Dockerfile
vendored
Normal file
8
.github/workflows/ronn/Dockerfile
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
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
Normal file
5
.github/workflows/ronn/Gemfile
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
source "https://rubygems.org"
|
||||
|
||||
gem "ronn", "~> 0.7.3"
|
||||
20
.github/workflows/ronn/Gemfile.lock
vendored
Normal file
20
.github/workflows/ronn/Gemfile.lock
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
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
Normal file
4
.github/workflows/ronn/action.yml
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
name: Ronn
|
||||
runs:
|
||||
using: docker
|
||||
image: Dockerfile
|
||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -6,7 +6,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
go: [1.15.x, 1.16.x]
|
||||
go: [1.16.x, 1.17.x]
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
|
||||
@@ -7,8 +7,9 @@
|
||||
class Age < Formula
|
||||
desc "Simple, modern, secure file encryption"
|
||||
homepage "https://filippo.io/age"
|
||||
url "https://github.com/FiloSottile/age/archive/v1.0.0-beta6.zip"
|
||||
sha256 "6ffa23aee0f03c3e00707915e4300591847a2b0c5157ca7a696eb39bfeb7359c"
|
||||
url "https://github.com/FiloSottile/age/archive/v1.0.0-rc.3.zip"
|
||||
sha256 "0e7d94f17e610d5ad9ce8e88e3c157b073dcc41984b1d07793aef44b9e3b67d8"
|
||||
head "https://github.com/FiloSottile/age.git"
|
||||
|
||||
depends_on "go" => :build
|
||||
|
||||
@@ -16,5 +17,7 @@ class Age < Formula
|
||||
mkdir bin
|
||||
system "go", "build", "-trimpath", "-o", bin, "-ldflags", "-X main.Version=v#{version}", "filippo.io/age/cmd/..."
|
||||
prefix.install_metafiles
|
||||
man1.install "doc/age.1"
|
||||
man1.install "doc/age-keygen.1"
|
||||
end
|
||||
end
|
||||
|
||||
144
README.md
144
README.md
@@ -1,8 +1,9 @@
|
||||
# age
|
||||
<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>
|
||||
|
||||
[](https://pkg.go.dev/filippo.io/age)
|
||||
[](https://pkg.go.dev/filippo.io/age)
|
||||
[](https://htmlpreview.github.io/?https://github.com/FiloSottile/age/blob/master/doc/age.1.html)
|
||||
|
||||
age is a simple, modern and secure file encryption tool, format, and library.
|
||||
age is a simple, modern and secure file encryption tool, format, and Go library.
|
||||
|
||||
It features small explicit keys, no config options, and UNIX-style composability.
|
||||
|
||||
@@ -13,28 +14,34 @@ $ tar cvz ~/data | age -r age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9
|
||||
$ age --decrypt -i key.txt data.tar.gz.age > data.tar.gz
|
||||
```
|
||||
|
||||
The format specification is at [age-encryption.org/v1](https://age-encryption.org/v1). To discuss the spec or other age related topics, please email [the mailing list](https://groups.google.com/d/forum/age-dev) at age-dev@googlegroups.com. age was designed by [@Benjojo12](https://twitter.com/Benjojo12) and [@FiloSottile](https://twitter.com/FiloSottile).
|
||||
The format specification is at [age-encryption.org/v1](https://age-encryption.org/v1). age was designed by [@Benjojo12](https://twitter.com/Benjojo12) and [@FiloSottile](https://twitter.com/FiloSottile).
|
||||
|
||||
An alternative interoperable Rust implementation is available at [github.com/str4d/rage](https://github.com/str4d/rage).
|
||||
|
||||
The author pronounces it `[aɡe̞]`, like the Italian [“aghe”](https://translate.google.com/?sl=it&text=aghe).
|
||||
|
||||
## Usage
|
||||
|
||||
For the full documentation, read [the age(1) man page](https://htmlpreview.github.io/?https://github.com/FiloSottile/age/blob/master/doc/age.1.html).
|
||||
|
||||
```
|
||||
Usage:
|
||||
age (-r RECIPIENT | -R PATH)... [--armor] [-o OUTPUT] [INPUT]
|
||||
age --passphrase [--armor] [-o OUTPUT] [INPUT]
|
||||
age [--encrypt] (-r RECIPIENT | -R PATH)... [--armor] [-o OUTPUT] [INPUT]
|
||||
age [--encrypt] --passphrase [--armor] [-o OUTPUT] [INPUT]
|
||||
age --decrypt [-i PATH]... [-o OUTPUT] [INPUT]
|
||||
|
||||
Options:
|
||||
-e, --encrypt Encrypt the input to the output. Default if omitted.
|
||||
-d, --decrypt Decrypt the input to the output.
|
||||
-o, --output OUTPUT Write the result to the file at path OUTPUT.
|
||||
-a, --armor Encrypt to a PEM encoded format.
|
||||
-p, --passphrase Encrypt with a passphrase.
|
||||
-r, --recipient RECIPIENT Encrypt to the specified RECIPIENT. Can be repeated.
|
||||
-R, --recipients-file PATH Encrypt to recipients listed at PATH. Can be repeated.
|
||||
-d, --decrypt Decrypt the input to the output.
|
||||
-i, --identity PATH Use the identity file at PATH. Can be repeated.
|
||||
|
||||
INPUT defaults to standard input, and OUTPUT defaults to standard output.
|
||||
If OUTPUT exists, it will be overwritten.
|
||||
|
||||
RECIPIENT can be an age public key generated by age-keygen ("age1...")
|
||||
or an SSH public key ("ssh-ed25519 AAAA...", "ssh-rsa AAAA...").
|
||||
@@ -45,8 +52,12 @@ read recipients from standard input.
|
||||
|
||||
Identity files contain one or more secret keys ("AGE-SECRET-KEY-1..."),
|
||||
one per line, or an SSH key. Empty lines and lines starting with "#" are
|
||||
ignored as comments. Multiple key files can be provided, and any unused ones
|
||||
ignored as comments. Passphrase encrypted age files can be used as
|
||||
identity files. Multiple key files can be provided, and any unused ones
|
||||
will be ignored. "-" may be used to read identities from standard input.
|
||||
|
||||
When --encrypt is specified explicitly, -i can also be used to encrypt to an
|
||||
identity file symmetrically, instead or in addition to normal recipients.
|
||||
```
|
||||
|
||||
### Multiple recipients
|
||||
@@ -85,6 +96,22 @@ $ age -d secrets.txt.age > secrets.txt
|
||||
Enter passphrase:
|
||||
```
|
||||
|
||||
### Passphrase-protected key files
|
||||
|
||||
If an identity file passed to `-i` is a passphrase encrypted age file, it will be automatically decrypted.
|
||||
|
||||
```
|
||||
$ age-keygen | age -p > key.age
|
||||
Public key: age1yhm4gctwfmrpz87tdslm550wrx6m79y9f2hdzt0lndjnehwj0ukqrjpyx5
|
||||
Enter passphrase (leave empty to autogenerate a secure one):
|
||||
Using the autogenerated passphrase "hip-roast-boring-snake-mention-east-wasp-honey-input-actress".
|
||||
$ age -r age1yhm4gctwfmrpz87tdslm550wrx6m79y9f2hdzt0lndjnehwj0ukqrjpyx5 secrets.txt > secrets.txt.age
|
||||
$ age -d -i key.age secrets.txt.age > secrets.txt
|
||||
Enter passphrase for identity file "key.age":
|
||||
```
|
||||
|
||||
Passphrase-protected identity files are not necessary for most use cases, where access to the encrypted identity file implies access to the whole system. However, they can be useful if the identity file is stored remotely.
|
||||
|
||||
### SSH keys
|
||||
|
||||
As a convenience feature, age also supports encrypting to `ssh-rsa` and `ssh-ed25519` SSH public keys, and decrypting with the respective private key file. (`ssh-agent` is not supported.)
|
||||
@@ -108,40 +135,89 @@ Keep in mind that people might not protect SSH keys long-term, since they are re
|
||||
|
||||
## Installation
|
||||
|
||||
On macOS or Linux, you can use Homebrew:
|
||||
<table>
|
||||
<tr>
|
||||
<td>Homebrew (macOS or Linux)</td>
|
||||
<td>
|
||||
<code>brew tap filippo.io/age https://filippo.io/age</code><br>
|
||||
<code>brew install age</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>MacPorts</td>
|
||||
<td>
|
||||
<code>port install age</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Ubuntu 21.04+</td>
|
||||
<td>
|
||||
<code>apt install age</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Debian 11+ (Bullseye)</td>
|
||||
<td>
|
||||
<code>apt install age</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Arch Linux</td>
|
||||
<td>
|
||||
<code>pacman -S age</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Fedora 33+</td>
|
||||
<td>
|
||||
<code>dnf install age</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>OpenBSD 6.7+</td>
|
||||
<td>
|
||||
<code>pkg_add age</code> (security/age)
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>FreeBSD</td>
|
||||
<td>
|
||||
<code>pkg install age</code> (security/age)
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>NixOS / Nix</td>
|
||||
<td>
|
||||
<code>nix-env -i age</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Gentoo Linux</td>
|
||||
<td>
|
||||
<code>emerge app-crypt/age</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Void Linux</td>
|
||||
<td>
|
||||
<code>xbps-install age</code>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
On Windows, Linux, macOS, and FreeBSD you can use the pre-built binaries.
|
||||
|
||||
```
|
||||
brew tap filippo.io/age https://filippo.io/age
|
||||
brew install age
|
||||
https://dl.filippo.io/age/latest?for=linux/amd64
|
||||
https://dl.filippo.io/age/v1.0.0-rc.1?for=darwin/arm64
|
||||
...
|
||||
```
|
||||
|
||||
On Windows, Linux, and macOS, you can use [the pre-built binaries](https://github.com/FiloSottile/age/releases).
|
||||
|
||||
If your system has [Go 1.13+](https://golang.org/dl/), you can build from source:
|
||||
If your system has [Go 1.13+](https://golang.org/dl/), you can build from source.
|
||||
|
||||
```
|
||||
git clone https://filippo.io/age && cd age
|
||||
go build -o . filippo.io/age/cmd/...
|
||||
```
|
||||
|
||||
On Arch Linux, age is available from AUR as [`age`](https://aur.archlinux.org/packages/age/) or [`age-git`](https://aur.archlinux.org/packages/age-git/):
|
||||
|
||||
```bash
|
||||
git clone https://aur.archlinux.org/age.git
|
||||
cd age
|
||||
makepkg -si
|
||||
```
|
||||
|
||||
On OpenBSD -current and 6.7+, you can use the port:
|
||||
|
||||
```
|
||||
pkg_add age
|
||||
```
|
||||
|
||||
On all supported versions of FreeBSD, you can build the security/age port or use pkg:
|
||||
|
||||
```
|
||||
pkg install age
|
||||
```
|
||||
|
||||
Help from new packagers is very welcome.
|
||||
|
||||
34
age.go
34
age.go
@@ -35,6 +35,16 @@
|
||||
// encryption operations. If you need to tie into existing key management
|
||||
// infrastructure, you might want to consider implementing your own Recipient
|
||||
// and Identity.
|
||||
//
|
||||
// Backwards compatibility
|
||||
//
|
||||
// Files encrypted with a stable version (not alpha, beta, or release candidate)
|
||||
// of age, or with any v1.0.0 beta or release candidate, will decrypt with any
|
||||
// later versions of the v1 API. This might change in v2, in which case v1 will
|
||||
// be maintained with security fixes for compatibility with older files.
|
||||
//
|
||||
// If decrypting an older file poses a security risk, doing so might require an
|
||||
// explicit opt-in in the API.
|
||||
package age
|
||||
|
||||
import (
|
||||
@@ -103,6 +113,16 @@ 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
|
||||
@@ -118,11 +138,6 @@ func Encrypt(dst io.Writer, recipients ...Recipient) (io.WriteCloser, error) {
|
||||
hdr.Recipients = append(hdr.Recipients, (*format.Stanza)(s))
|
||||
}
|
||||
}
|
||||
for _, s := range hdr.Recipients {
|
||||
if s.Type == "scrypt" && len(hdr.Recipients) != 1 {
|
||||
return nil, errors.New("an scrypt recipient must be the only one")
|
||||
}
|
||||
}
|
||||
if mac, err := headerMAC(fileKey, hdr); err != nil {
|
||||
return nil, fmt.Errorf("failed to compute header MAC: %v", err)
|
||||
} else {
|
||||
@@ -168,15 +183,6 @@ func Decrypt(src io.Reader, identities ...Identity) (io.Reader, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read header: %v", err)
|
||||
}
|
||||
if len(hdr.Recipients) > 20 {
|
||||
return nil, errors.New("too many recipients")
|
||||
}
|
||||
|
||||
for _, r := range hdr.Recipients {
|
||||
if r.Type == "scrypt" && len(hdr.Recipients) != 1 {
|
||||
return nil, errors.New("an scrypt recipient must be the only one")
|
||||
}
|
||||
}
|
||||
|
||||
stanzas := make([]*Stanza, 0, len(hdr.Recipients))
|
||||
for _, s := range hdr.Recipients {
|
||||
|
||||
@@ -24,10 +24,10 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
|
||||
"filippo.io/age"
|
||||
"filippo.io/age/internal/format"
|
||||
"filippo.io/edwards25519"
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
"golang.org/x/crypto/curve25519"
|
||||
"golang.org/x/crypto/hkdf"
|
||||
@@ -65,6 +65,9 @@ func NewRSARecipient(pk ssh.PublicKey) (*RSARecipient, error) {
|
||||
} else {
|
||||
return nil, errors.New("pk does not implement ssh.CryptoPublicKey")
|
||||
}
|
||||
if r.pubKey.Size() < 2048/8 {
|
||||
return nil, errors.New("RSA key size is too small")
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
@@ -186,37 +189,14 @@ func ParseRecipient(s string) (age.Recipient, error) {
|
||||
return r, nil
|
||||
}
|
||||
|
||||
var curve25519P, _ = new(big.Int).SetString("57896044618658097711785492504343953926634992332820282019728792003956564819949", 10)
|
||||
|
||||
func ed25519PublicKeyToCurve25519(pk ed25519.PublicKey) ([]byte, error) {
|
||||
// ed25519.PublicKey is a little endian representation of the y-coordinate,
|
||||
// with the most significant bit set based on the sign of the x-coordinate.
|
||||
bigEndianY := make([]byte, ed25519.PublicKeySize)
|
||||
for i, b := range pk {
|
||||
bigEndianY[ed25519.PublicKeySize-i-1] = b
|
||||
// See https://blog.filippo.io/using-ed25519-keys-for-encryption and
|
||||
// https://pkg.go.dev/filippo.io/edwards25519#Point.BytesMontgomery.
|
||||
p, err := new(edwards25519.Point).SetBytes(pk)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bigEndianY[0] &= 0b0111_1111
|
||||
|
||||
// The Montgomery u-coordinate is derived through the bilinear map
|
||||
//
|
||||
// u = (1 + y) / (1 - y)
|
||||
//
|
||||
// See https://blog.filippo.io/using-ed25519-keys-for-encryption.
|
||||
y := new(big.Int).SetBytes(bigEndianY)
|
||||
denom := new(big.Int).Sub(big.NewInt(1), y)
|
||||
if denom = denom.ModInverse(denom, curve25519P); denom == nil {
|
||||
return nil, errors.New("invalid point")
|
||||
}
|
||||
u := y.Mul(y.Add(y, big.NewInt(1)), denom)
|
||||
u.Mod(u, curve25519P)
|
||||
|
||||
out := make([]byte, curve25519.PointSize)
|
||||
uBytes := u.Bytes()
|
||||
for i, b := range uBytes {
|
||||
out[len(uBytes)-i-1] = b
|
||||
}
|
||||
|
||||
return out, nil
|
||||
return p.BytesMontgomery(), nil
|
||||
}
|
||||
|
||||
const ed25519Label = "age-encryption.org/v1/ssh-ed25519"
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
)
|
||||
|
||||
func TestSSHRSARoundTrip(t *testing.T) {
|
||||
pk, err := rsa.GenerateKey(rand.Reader, 768)
|
||||
pk, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
// pass the result to NewEd25519Identity or NewRSAIdentity.
|
||||
type EncryptedSSHIdentity struct {
|
||||
pubKey ssh.PublicKey
|
||||
recipient age.Recipient
|
||||
pemBytes []byte
|
||||
passphrase func() ([]byte, error)
|
||||
|
||||
@@ -41,22 +42,34 @@ type EncryptedSSHIdentity struct {
|
||||
// passphrase is a callback that will be invoked by Unwrap when the passphrase
|
||||
// is necessary.
|
||||
func NewEncryptedSSHIdentity(pubKey ssh.PublicKey, pemBytes []byte, passphrase func() ([]byte, error)) (*EncryptedSSHIdentity, error) {
|
||||
switch t := pubKey.Type(); t {
|
||||
case "ssh-ed25519", "ssh-rsa":
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported SSH key type: %v", t)
|
||||
}
|
||||
return &EncryptedSSHIdentity{
|
||||
i := &EncryptedSSHIdentity{
|
||||
pubKey: pubKey,
|
||||
pemBytes: pemBytes,
|
||||
passphrase: passphrase,
|
||||
}, nil
|
||||
}
|
||||
switch t := pubKey.Type(); t {
|
||||
case "ssh-ed25519":
|
||||
r, err := NewEd25519Recipient(pubKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
i.recipient = r
|
||||
case "ssh-rsa":
|
||||
r, err := NewRSARecipient(pubKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
i.recipient = r
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported SSH key type: %v", t)
|
||||
}
|
||||
return i, nil
|
||||
}
|
||||
|
||||
var _ age.Identity = &EncryptedSSHIdentity{}
|
||||
|
||||
func (i *EncryptedSSHIdentity) Recipient() (age.Recipient, error) {
|
||||
return ParseRecipient(string(ssh.MarshalAuthorizedKey(i.pubKey)))
|
||||
func (i *EncryptedSSHIdentity) Recipient() age.Recipient {
|
||||
return i.recipient
|
||||
}
|
||||
|
||||
// Unwrap implements age.Identity. If the private key is still encrypted, and
|
||||
|
||||
@@ -28,7 +28,7 @@ const (
|
||||
|
||||
type armoredWriter struct {
|
||||
started, closed bool
|
||||
encoder io.WriteCloser
|
||||
encoder *format.WrappedBase64Encoder
|
||||
dst io.Writer
|
||||
}
|
||||
|
||||
@@ -50,15 +50,20 @@ func (a *armoredWriter) Close() error {
|
||||
if err := a.encoder.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := io.WriteString(a.dst, "\n"+Footer+"\n")
|
||||
footer := Footer + "\n"
|
||||
if !a.encoder.LastLineIsEmpty() {
|
||||
footer = "\n" + footer
|
||||
}
|
||||
_, err := io.WriteString(a.dst, footer)
|
||||
return err
|
||||
}
|
||||
|
||||
func NewWriter(dst io.Writer) io.WriteCloser {
|
||||
// TODO: write a test with aligned and misaligned sizes, and 8 and 10 steps.
|
||||
return &armoredWriter{dst: dst,
|
||||
encoder: base64.NewEncoder(base64.StdEncoding.Strict(),
|
||||
format.NewlineWriter(dst))}
|
||||
return &armoredWriter{
|
||||
dst: dst,
|
||||
encoder: format.NewWrappedBase64Encoder(base64.StdEncoding, dst),
|
||||
}
|
||||
}
|
||||
|
||||
type armoredReader struct {
|
||||
|
||||
@@ -8,6 +8,7 @@ package armor_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -18,6 +19,7 @@ import (
|
||||
|
||||
"filippo.io/age"
|
||||
"filippo.io/age/armor"
|
||||
"filippo.io/age/internal/format"
|
||||
)
|
||||
|
||||
func ExampleNewWriter() {
|
||||
@@ -87,9 +89,15 @@ kB/RRusYjn+KVJ+KTioxj0THtzZPXcjFKuQ1
|
||||
}
|
||||
|
||||
func TestArmor(t *testing.T) {
|
||||
t.Run("PartialLine", func(t *testing.T) { testArmor(t, 611) })
|
||||
t.Run("FullLine", func(t *testing.T) { testArmor(t, 10*format.BytesPerLine) })
|
||||
}
|
||||
|
||||
func testArmor(t *testing.T, size int) {
|
||||
buf := &bytes.Buffer{}
|
||||
w := armor.NewWriter(buf)
|
||||
plain := make([]byte, 611)
|
||||
plain := make([]byte, size)
|
||||
rand.Read(plain)
|
||||
if _, err := w.Write(plain); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -101,9 +109,18 @@ func TestArmor(t *testing.T) {
|
||||
if block == nil {
|
||||
t.Fatal("PEM decoding failed")
|
||||
}
|
||||
if len(block.Headers) != 0 {
|
||||
t.Error("unexpected headers")
|
||||
}
|
||||
if block.Type != "AGE ENCRYPTED FILE" {
|
||||
t.Errorf("unexpected type %q", block.Type)
|
||||
}
|
||||
if !bytes.Equal(block.Bytes, plain) {
|
||||
t.Error("PEM decoded value doesn't match")
|
||||
}
|
||||
if !bytes.Equal(buf.Bytes(), pem.EncodeToMemory(block)) {
|
||||
t.Error("PEM re-encoded value doesn't match")
|
||||
}
|
||||
|
||||
r := armor.NewReader(buf)
|
||||
out, err := ioutil.ReadAll(r)
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
"time"
|
||||
|
||||
"filippo.io/age"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
const usage = `Usage:
|
||||
@@ -27,7 +27,7 @@ Options:
|
||||
-o, --output OUTPUT Write the result to the file at path OUTPUT.
|
||||
-y Convert an identity file to a recipients file.
|
||||
|
||||
age-keygen generates a new standard X25519 key pair, and outputs it to
|
||||
age-keygen generates a new native X25519 key pair, and outputs it to
|
||||
standard output or to the OUTPUT file.
|
||||
|
||||
If an OUTPUT file is specified, the public key is printed to standard error.
|
||||
@@ -70,10 +70,10 @@ func main() {
|
||||
flag.StringVar(&outFlag, "output", "", "output to `FILE` (default stdout)")
|
||||
flag.Parse()
|
||||
if len(flag.Args()) != 0 && !convertFlag {
|
||||
log.Fatalf("age-keygen takes no arguments")
|
||||
errorf("too many arguments")
|
||||
}
|
||||
if len(flag.Args()) > 1 && convertFlag {
|
||||
log.Fatalf("Too many arguments")
|
||||
errorf("too many arguments")
|
||||
}
|
||||
if versionFlag {
|
||||
if Version != "" {
|
||||
@@ -92,23 +92,21 @@ func main() {
|
||||
if outFlag != "" {
|
||||
f, err := os.OpenFile(outFlag, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to open output file %q: %v", outFlag, err)
|
||||
errorf("failed to open output file %q: %v", outFlag, err)
|
||||
}
|
||||
defer f.Close()
|
||||
defer func() {
|
||||
if err := f.Close(); err != nil {
|
||||
errorf("failed to close output file %q: %v", outFlag, err)
|
||||
}
|
||||
}()
|
||||
out = f
|
||||
}
|
||||
|
||||
if fi, err := out.Stat(); err == nil {
|
||||
if fi.Mode().IsRegular() && fi.Mode().Perm()&0004 != 0 {
|
||||
fmt.Fprintf(os.Stderr, "Warning: writing secret key to a world-readable file.\n")
|
||||
}
|
||||
}
|
||||
|
||||
in := os.Stdin
|
||||
if inFile := flag.Arg(0); inFile != "" && inFile != "-" {
|
||||
f, err := os.Open(inFile)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to open input file %q: %v", inFile, err)
|
||||
errorf("failed to open input file %q: %v", inFile, err)
|
||||
}
|
||||
defer f.Close()
|
||||
in = f
|
||||
@@ -117,6 +115,9 @@ func main() {
|
||||
if convertFlag {
|
||||
convert(in, out)
|
||||
} else {
|
||||
if fi, err := out.Stat(); err == nil && fi.Mode().IsRegular() && fi.Mode().Perm()&0004 != 0 {
|
||||
warning("writing secret key to a world-readable file")
|
||||
}
|
||||
generate(out)
|
||||
}
|
||||
}
|
||||
@@ -124,10 +125,10 @@ func main() {
|
||||
func generate(out *os.File) {
|
||||
k, err := age.GenerateX25519Identity()
|
||||
if err != nil {
|
||||
log.Fatalf("Internal error: %v", err)
|
||||
errorf("internal error: %v", err)
|
||||
}
|
||||
|
||||
if !terminal.IsTerminal(int(out.Fd())) {
|
||||
if !term.IsTerminal(int(out.Fd())) {
|
||||
fmt.Fprintf(os.Stderr, "Public key: %s\n", k.Recipient())
|
||||
}
|
||||
|
||||
@@ -139,16 +140,24 @@ func generate(out *os.File) {
|
||||
func convert(in io.Reader, out io.Writer) {
|
||||
ids, err := age.ParseIdentities(in)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to parse input: %v", err)
|
||||
errorf("failed to parse input: %v", err)
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
log.Fatalf("No identities found in the input")
|
||||
errorf("no identities found in the input")
|
||||
}
|
||||
for _, id := range ids {
|
||||
id, ok := id.(*age.X25519Identity)
|
||||
if !ok {
|
||||
log.Fatalf("Internal error: unexpected identity type: %T", id)
|
||||
errorf("internal error: unexpected identity type: %T", id)
|
||||
}
|
||||
fmt.Fprintf(out, "%s\n", id.Recipient())
|
||||
}
|
||||
}
|
||||
|
||||
func errorf(format string, v ...interface{}) {
|
||||
log.Printf("age-keygen: error: "+format, v...)
|
||||
}
|
||||
|
||||
func warning(msg string) {
|
||||
log.Printf("age-keygen: warning: " + msg)
|
||||
}
|
||||
|
||||
163
cmd/age/age.go
163
cmd/age/age.go
@@ -12,7 +12,7 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
_log "log"
|
||||
"log"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
"filippo.io/age"
|
||||
"filippo.io/age/agessh"
|
||||
"filippo.io/age/armor"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
type multiFlag []string
|
||||
@@ -59,7 +59,8 @@ read recipients from standard input.
|
||||
|
||||
Identity files contain one or more secret keys ("AGE-SECRET-KEY-1..."),
|
||||
one per line, or an SSH key. Empty lines and lines starting with "#" are
|
||||
ignored as comments. Multiple key files can be provided, and any unused ones
|
||||
ignored as comments. Passphrase encrypted age files can be used as
|
||||
identity files. Multiple key files can be provided, and any unused ones
|
||||
will be ignored. "-" may be used to read identities from standard input.
|
||||
|
||||
When --encrypt is specified explicitly, -i can also be used to encrypt to an
|
||||
@@ -77,7 +78,7 @@ Example:
|
||||
var Version string
|
||||
|
||||
func main() {
|
||||
_log.SetFlags(0)
|
||||
log.SetFlags(0)
|
||||
flag.Usage = func() { fmt.Fprintf(os.Stderr, "%s\n", usage) }
|
||||
|
||||
if len(os.Args) == 1 {
|
||||
@@ -126,47 +127,47 @@ func main() {
|
||||
}
|
||||
|
||||
if flag.NArg() > 1 {
|
||||
logFatalf("Error: too many arguments: %q.\n"+
|
||||
"Note that the input file must be specified after all flags.", flag.Args())
|
||||
errorWithHint(fmt.Sprintf("too many arguments: %q", flag.Args()),
|
||||
"note that the input file must be specified after all flags")
|
||||
}
|
||||
switch {
|
||||
case decryptFlag:
|
||||
if encryptFlag {
|
||||
logFatalf("Error: -e/--encrypt can't be used with -d/--decrypt.")
|
||||
errorf("-e/--encrypt can't be used with -d/--decrypt")
|
||||
}
|
||||
if armorFlag {
|
||||
logFatalf("Error: -a/--armor can't be used with -d/--decrypt.\n" +
|
||||
"Note that armored files are detected automatically.")
|
||||
errorWithHint("-a/--armor can't be used with -d/--decrypt",
|
||||
"note that armored files are detected automatically")
|
||||
}
|
||||
if passFlag {
|
||||
logFatalf("Error: -p/--passphrase can't be used with -d/--decrypt.\n" +
|
||||
"Note that password protected files are detected automatically.")
|
||||
errorWithHint("-p/--passphrase can't be used with -d/--decrypt",
|
||||
"note that password protected files are detected automatically")
|
||||
}
|
||||
if len(recipientFlags) > 0 {
|
||||
logFatalf("Error: -r/--recipient can't be used with -d/--decrypt.\n" +
|
||||
"Did you mean to use -i/--identity to specify a private key?")
|
||||
errorWithHint("-r/--recipient can't be used with -d/--decrypt",
|
||||
"did you mean to use -i/--identity to specify a private key?")
|
||||
}
|
||||
if len(recipientsFileFlags) > 0 {
|
||||
logFatalf("Error: -R/--recipients-file can't be used with -d/--decrypt.\n" +
|
||||
"Did you mean to use -i/--identity to specify a private key?")
|
||||
errorWithHint("-R/--recipients-file can't be used with -d/--decrypt",
|
||||
"did you mean to use -i/--identity to specify a private key?")
|
||||
}
|
||||
default: // encrypt
|
||||
if len(identityFlags) > 0 && !encryptFlag {
|
||||
logFatalf("Error: -i/--identity can't be used in encryption mode unless symmetric encryption is explicitly selected with -e/--encrypt.\n" +
|
||||
"Did you forget to specify -d/--decrypt?")
|
||||
errorWithHint("-i/--identity can't be used in encryption mode unless symmetric encryption is explicitly selected with -e/--encrypt",
|
||||
"did you forget to specify -d/--decrypt?")
|
||||
}
|
||||
if len(recipientFlags)+len(recipientsFileFlags)+len(identityFlags) == 0 && !passFlag {
|
||||
logFatalf("Error: missing recipients.\n" +
|
||||
"Did you forget to specify -r/--recipient, -R/--recipients-file or -p/--passphrase?")
|
||||
errorWithHint("missing recipients",
|
||||
"did you forget to specify -r/--recipient, -R/--recipients-file or -p/--passphrase?")
|
||||
}
|
||||
if len(recipientFlags) > 0 && passFlag {
|
||||
logFatalf("Error: -p/--passphrase can't be combined with -r/--recipient.")
|
||||
errorf("-p/--passphrase can't be combined with -r/--recipient")
|
||||
}
|
||||
if len(recipientsFileFlags) > 0 && passFlag {
|
||||
logFatalf("Error: -p/--passphrase can't be combined with -R/--recipients-file.")
|
||||
errorf("-p/--passphrase can't be combined with -R/--recipients-file")
|
||||
}
|
||||
if len(identityFlags) > 0 && passFlag {
|
||||
logFatalf("Error: -p/--passphrase can't be combined with -i/--identity.")
|
||||
errorf("-p/--passphrase can't be combined with -i/--identity")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,7 +176,7 @@ func main() {
|
||||
if name := flag.Arg(0); name != "" && name != "-" {
|
||||
f, err := os.Open(name)
|
||||
if err != nil {
|
||||
logFatalf("Error: failed to open input file %q: %v", name, err)
|
||||
errorf("failed to open input file %q: %v", name, err)
|
||||
}
|
||||
defer f.Close()
|
||||
in = f
|
||||
@@ -184,20 +185,25 @@ func main() {
|
||||
}
|
||||
if name := outFlag; name != "" && name != "-" {
|
||||
f := newLazyOpener(name)
|
||||
defer f.Close()
|
||||
defer func() {
|
||||
if err := f.Close(); err != nil {
|
||||
errorf("failed to close output file %q: %v", name, err)
|
||||
}
|
||||
}()
|
||||
out = f
|
||||
} else if terminal.IsTerminal(int(os.Stdout.Fd())) {
|
||||
} else if term.IsTerminal(int(os.Stdout.Fd())) {
|
||||
if name != "-" {
|
||||
if decryptFlag {
|
||||
// TODO: buffer the output and check it's printable.
|
||||
} else if !armorFlag {
|
||||
// If the output wouldn't be armored, refuse to send binary to
|
||||
// the terminal unless explicitly requested with "-o -".
|
||||
logFatalf("Error: refusing to output binary to the terminal.\n" +
|
||||
`Did you mean to use -a/--armor? Force with "-o -".`)
|
||||
errorWithHint("refusing to output binary to the terminal",
|
||||
"did you mean to use -a/--armor?",
|
||||
`force anyway with "-o -"`)
|
||||
}
|
||||
}
|
||||
if in == os.Stdin && terminal.IsTerminal(int(os.Stdin.Fd())) {
|
||||
if in == os.Stdin && term.IsTerminal(int(os.Stdin.Fd())) {
|
||||
// If the input comes from a TTY and output will go to a TTY,
|
||||
// buffer it up so it doesn't get in the way of typing the input.
|
||||
buf := &bytes.Buffer{}
|
||||
@@ -212,7 +218,7 @@ func main() {
|
||||
case passFlag:
|
||||
pass, err := passphrasePromptForEncryption()
|
||||
if err != nil {
|
||||
logFatalf("Error: %v", err)
|
||||
errorf("%v", err)
|
||||
}
|
||||
encryptPass(pass, in, out, armorFlag)
|
||||
default:
|
||||
@@ -221,8 +227,7 @@ func main() {
|
||||
}
|
||||
|
||||
func passphrasePromptForEncryption() (string, error) {
|
||||
fmt.Fprintf(os.Stderr, "Enter passphrase (leave empty to autogenerate a secure one): ")
|
||||
pass, err := readPassphrase()
|
||||
pass, err := readPassphrase("Enter passphrase (leave empty to autogenerate a secure one):")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not read passphrase: %v", err)
|
||||
}
|
||||
@@ -233,10 +238,10 @@ func passphrasePromptForEncryption() (string, error) {
|
||||
words = append(words, randomWord())
|
||||
}
|
||||
p = strings.Join(words, "-")
|
||||
// TODO: consider printing this to the terminal, instead of stderr.
|
||||
fmt.Fprintf(os.Stderr, "Using the autogenerated passphrase %q.\n", p)
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "Confirm passphrase: ")
|
||||
confirm, err := readPassphrase()
|
||||
confirm, err := readPassphrase("Confirm passphrase:")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not read passphrase: %v", err)
|
||||
}
|
||||
@@ -251,30 +256,33 @@ func encryptKeys(keys, files, identities []string, in io.Reader, out io.Writer,
|
||||
var recipients []age.Recipient
|
||||
for _, arg := range keys {
|
||||
r, err := parseRecipient(arg)
|
||||
if err, ok := err.(gitHubRecipientError); ok {
|
||||
errorWithHint(err.Error(), "instead, use recipient files like",
|
||||
" curl -O https://github.com/"+err.username+".keys",
|
||||
" age -R "+err.username+".keys")
|
||||
}
|
||||
if err != nil {
|
||||
logFatalf("Error: %v", err)
|
||||
errorf("%v", err)
|
||||
}
|
||||
recipients = append(recipients, r)
|
||||
}
|
||||
for _, name := range files {
|
||||
recs, err := parseRecipientsFile(name)
|
||||
if err != nil {
|
||||
logFatalf("Error: failed to parse recipient file %q: %v", name, err)
|
||||
errorf("failed to parse recipient file %q: %v", name, err)
|
||||
}
|
||||
recipients = append(recipients, recs...)
|
||||
}
|
||||
for _, name := range identities {
|
||||
ids, err := parseIdentitiesFile(name)
|
||||
if err != nil {
|
||||
logFatalf("Error reading %q: %v", name, err)
|
||||
errorf("reading %q: %v", name, err)
|
||||
}
|
||||
for _, id := range ids {
|
||||
r, err := identityToRecipient(id)
|
||||
if err != nil {
|
||||
logFatalf("Internal error processing %q: %v", name, err)
|
||||
}
|
||||
recipients = append(recipients, r)
|
||||
r, err := identitiesToRecipients(ids)
|
||||
if err != nil {
|
||||
errorf("internal error processing %q: %v", name, err)
|
||||
}
|
||||
recipients = append(recipients, r...)
|
||||
}
|
||||
encrypt(recipients, in, out, armor)
|
||||
}
|
||||
@@ -282,7 +290,7 @@ func encryptKeys(keys, files, identities []string, in io.Reader, out io.Writer,
|
||||
func encryptPass(pass string, in io.Reader, out io.Writer, armor bool) {
|
||||
r, err := age.NewScryptRecipient(pass)
|
||||
if err != nil {
|
||||
logFatalf("Error: %v", err)
|
||||
errorf("%v", err)
|
||||
}
|
||||
encrypt([]age.Recipient{r}, in, out, armor)
|
||||
}
|
||||
@@ -292,20 +300,20 @@ func encrypt(recipients []age.Recipient, in io.Reader, out io.Writer, withArmor
|
||||
a := armor.NewWriter(out)
|
||||
defer func() {
|
||||
if err := a.Close(); err != nil {
|
||||
logFatalf("Error: %v", err)
|
||||
errorf("%v", err)
|
||||
}
|
||||
}()
|
||||
out = a
|
||||
}
|
||||
w, err := age.Encrypt(out, recipients...)
|
||||
if err != nil {
|
||||
logFatalf("Error: %v", err)
|
||||
errorf("%v", err)
|
||||
}
|
||||
if _, err := io.Copy(w, in); err != nil {
|
||||
logFatalf("Error: %v", err)
|
||||
errorf("%v", err)
|
||||
}
|
||||
if err := w.Close(); err != nil {
|
||||
logFatalf("Error: %v", err)
|
||||
errorf("%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,7 +327,7 @@ func decrypt(keys []string, in io.Reader, out io.Writer) {
|
||||
for _, name := range keys {
|
||||
ids, err := parseIdentitiesFile(name)
|
||||
if err != nil {
|
||||
logFatalf("Error reading %q: %v", name, err)
|
||||
errorf("reading %q: %v", name, err)
|
||||
}
|
||||
identities = append(identities, ids...)
|
||||
}
|
||||
@@ -333,34 +341,44 @@ func decrypt(keys []string, in io.Reader, out io.Writer) {
|
||||
|
||||
r, err := age.Decrypt(in, identities...)
|
||||
if err != nil {
|
||||
logFatalf("Error: %v", err)
|
||||
errorf("%v", err)
|
||||
}
|
||||
if _, err := io.Copy(out, r); err != nil {
|
||||
logFatalf("Error: %v", err)
|
||||
errorf("%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func passphrasePrompt() (string, error) {
|
||||
fmt.Fprintf(os.Stderr, "Enter passphrase: ")
|
||||
pass, err := readPassphrase()
|
||||
pass, err := readPassphrase("Enter passphrase:")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not read passphrase: %v", err)
|
||||
}
|
||||
return string(pass), nil
|
||||
}
|
||||
|
||||
func identityToRecipient(id age.Identity) (age.Recipient, error) {
|
||||
switch id := id.(type) {
|
||||
case *age.X25519Identity:
|
||||
return id.Recipient(), nil
|
||||
case *agessh.RSAIdentity:
|
||||
return id.Recipient(), nil
|
||||
case *agessh.Ed25519Identity:
|
||||
return id.Recipient(), nil
|
||||
case *agessh.EncryptedSSHIdentity:
|
||||
return id.Recipient()
|
||||
func identitiesToRecipients(ids []age.Identity) ([]age.Recipient, error) {
|
||||
var recipients []age.Recipient
|
||||
for _, id := range ids {
|
||||
switch id := id.(type) {
|
||||
case *age.X25519Identity:
|
||||
recipients = append(recipients, id.Recipient())
|
||||
case *agessh.RSAIdentity:
|
||||
recipients = append(recipients, id.Recipient())
|
||||
case *agessh.Ed25519Identity:
|
||||
recipients = append(recipients, id.Recipient())
|
||||
case *agessh.EncryptedSSHIdentity:
|
||||
recipients = append(recipients, id.Recipient())
|
||||
case *EncryptedIdentity:
|
||||
r, err := id.Recipients()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
recipients = append(recipients, r...)
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected identity type: %T", id)
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected identity type: %T", id)
|
||||
return recipients, nil
|
||||
}
|
||||
|
||||
type lazyOpener struct {
|
||||
@@ -390,8 +408,19 @@ func (l *lazyOpener) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func logFatalf(format string, v ...interface{}) {
|
||||
_log.Printf(format, v...)
|
||||
_log.Fatalf("[ Did age not do what you expected? Could an error be more useful?" +
|
||||
" Tell us: https://filippo.io/age/report ]")
|
||||
func errorf(format string, v ...interface{}) {
|
||||
log.Printf("age: error: "+format, v...)
|
||||
log.Fatalf("age: report unexpected or unhelpful errors at https://filippo.io/age/report")
|
||||
}
|
||||
|
||||
func warningf(format string, v ...interface{}) {
|
||||
log.Printf("age: warning: "+format, v...)
|
||||
}
|
||||
|
||||
func errorWithHint(error string, hints ...string) {
|
||||
log.Printf("age: error: %s", error)
|
||||
for _, hint := range hints {
|
||||
log.Printf("age: hint: %s", hint)
|
||||
}
|
||||
log.Fatalf("age: report unexpected or unhelpful errors at https://filippo.io/age/report")
|
||||
}
|
||||
|
||||
@@ -18,24 +18,30 @@ import (
|
||||
)
|
||||
|
||||
func TestVectors(t *testing.T) {
|
||||
defaultIDs, err := parseIdentitiesFile("testdata/default_key.txt")
|
||||
var defaultIDs []age.Identity
|
||||
|
||||
password, err := ioutil.ReadFile("testdata/default_password.txt")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
password, err := ioutil.ReadFile("testdata/default_password.txt")
|
||||
if err == nil {
|
||||
p := strings.TrimSpace(string(password))
|
||||
i, err := age.NewScryptIdentity(p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defaultIDs = append(defaultIDs, i)
|
||||
p := strings.TrimSpace(string(password))
|
||||
i, err := age.NewScryptIdentity(p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defaultIDs = append(defaultIDs, i)
|
||||
|
||||
ids, err := parseIdentitiesFile("testdata/default_key.txt")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defaultIDs = append(defaultIDs, ids...)
|
||||
|
||||
files, _ := filepath.Glob("testdata/*.age")
|
||||
for _, f := range files {
|
||||
_, name := filepath.Split(f)
|
||||
name = strings.TrimSuffix(name, ".age")
|
||||
expectPass := strings.HasPrefix(name, "good_")
|
||||
expectFailure := strings.HasPrefix(name, "fail_")
|
||||
expectNoMatch := strings.HasPrefix(name, "nomatch_")
|
||||
t.Run(name, func(t *testing.T) {
|
||||
@@ -63,17 +69,17 @@ func TestVectors(t *testing.T) {
|
||||
if err == nil {
|
||||
t.Fatal("expected Decrypt failure")
|
||||
}
|
||||
if e := (&age.NoIdentityMatchError{}); errors.As(err, &e) {
|
||||
if e := new(age.NoIdentityMatchError); errors.As(err, &e) {
|
||||
t.Errorf("got ErrIncorrectIdentity, expected more specific error")
|
||||
}
|
||||
} else if expectNoMatch {
|
||||
if err == nil {
|
||||
t.Fatal("expected Decrypt failure")
|
||||
}
|
||||
if e := (&age.NoIdentityMatchError{}); !errors.As(err, &e) {
|
||||
if e := new(age.NoIdentityMatchError); !errors.As(err, &e) {
|
||||
t.Errorf("expected ErrIncorrectIdentity, got %v", err)
|
||||
}
|
||||
} else {
|
||||
} else if expectPass {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -82,6 +88,8 @@ func TestVectors(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("%s", out)
|
||||
} else {
|
||||
t.Fatal("invalid test vector")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,12 +7,14 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"filippo.io/age"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
type LazyScryptIdentity struct {
|
||||
@@ -22,6 +24,11 @@ type LazyScryptIdentity struct {
|
||||
var _ age.Identity = &LazyScryptIdentity{}
|
||||
|
||||
func (i *LazyScryptIdentity) Unwrap(stanzas []*age.Stanza) (fileKey []byte, err error) {
|
||||
for _, s := range stanzas {
|
||||
if s.Type == "scrypt" && len(stanzas) != 1 {
|
||||
return nil, errors.New("an scrypt recipient must be the only one")
|
||||
}
|
||||
}
|
||||
if len(stanzas) != 1 || stanzas[0].Type != "scrypt" {
|
||||
return nil, age.ErrIncorrectIdentity
|
||||
}
|
||||
@@ -45,23 +52,93 @@ func (i *LazyScryptIdentity) Unwrap(stanzas []*age.Stanza) (fileKey []byte, err
|
||||
return fileKey, err
|
||||
}
|
||||
|
||||
// stdinInUse is set in main. It's a singleton like os.Stdin.
|
||||
var stdinInUse bool
|
||||
type EncryptedIdentity struct {
|
||||
Contents []byte
|
||||
Passphrase func() (string, error)
|
||||
NoMatchWarning func()
|
||||
|
||||
func readPassphrase() ([]byte, error) {
|
||||
fd := int(os.Stdin.Fd())
|
||||
if !terminal.IsTerminal(fd) || stdinInUse {
|
||||
tty, err := os.Open("/dev/tty")
|
||||
identities []age.Identity
|
||||
}
|
||||
|
||||
var _ age.Identity = &EncryptedIdentity{}
|
||||
|
||||
func (i *EncryptedIdentity) Recipients() ([]age.Recipient, error) {
|
||||
if i.identities == nil {
|
||||
if err := i.decrypt(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return identitiesToRecipients(i.identities)
|
||||
}
|
||||
|
||||
func (i *EncryptedIdentity) Unwrap(stanzas []*age.Stanza) (fileKey []byte, err error) {
|
||||
if i.identities == nil {
|
||||
if err := i.decrypt(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
for _, id := range i.identities {
|
||||
fileKey, err = id.Unwrap(stanzas)
|
||||
if errors.Is(err, age.ErrIncorrectIdentity) {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("standard input is not available or not a terminal, and opening /dev/tty failed: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
return fileKey, nil
|
||||
}
|
||||
i.NoMatchWarning()
|
||||
return nil, age.ErrIncorrectIdentity
|
||||
}
|
||||
|
||||
func (i *EncryptedIdentity) decrypt() error {
|
||||
d, err := age.Decrypt(bytes.NewReader(i.Contents), &LazyScryptIdentity{i.Passphrase})
|
||||
if e := new(age.NoIdentityMatchError); errors.As(err, &e) {
|
||||
return fmt.Errorf("identity file is encrypted with age but not with a passphrase")
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decrypt identity file: %v", err)
|
||||
}
|
||||
i.identities, err = age.ParseIdentities(d)
|
||||
return err
|
||||
}
|
||||
|
||||
// readPassphrase reads a passphrase from the terminal. It does not read from a
|
||||
// non-terminal stdin, so it does not check stdinInUse.
|
||||
func readPassphrase(prompt string) ([]byte, error) {
|
||||
var in, out *os.File
|
||||
if runtime.GOOS == "windows" {
|
||||
var err error
|
||||
in, err = os.OpenFile("CONIN$", os.O_RDWR, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer in.Close()
|
||||
out, err = os.OpenFile("CONOUT$", os.O_WRONLY, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer out.Close()
|
||||
} else if _, err := os.Stat("/dev/tty"); err == nil {
|
||||
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tty.Close()
|
||||
fd = int(tty.Fd())
|
||||
in, out = tty, tty
|
||||
} else {
|
||||
if !term.IsTerminal(int(os.Stdin.Fd())) {
|
||||
return nil, fmt.Errorf("standard input is not a terminal, and /dev/tty is not available: %v", err)
|
||||
}
|
||||
in, out = os.Stdin, os.Stderr
|
||||
}
|
||||
defer fmt.Fprintf(os.Stderr, "\n")
|
||||
p, err := terminal.ReadPassword(fd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return p, nil
|
||||
fmt.Fprintf(out, "%s ", prompt)
|
||||
// Use CRLF to work around an apparent bug in WSL2's handling of CONOUT$.
|
||||
// Only when running a Windows binary from WSL2, the cursor would not go
|
||||
// back to the start of the line with a simple LF. Honestly, it's impressive
|
||||
// CONIN$ and CONOUT$ even work at all inside WSL2.
|
||||
defer fmt.Fprintf(out, "\r\n")
|
||||
return term.ReadPassword(int(in.Fd()))
|
||||
}
|
||||
|
||||
@@ -12,16 +12,27 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"filippo.io/age"
|
||||
"filippo.io/age/agessh"
|
||||
"filippo.io/age/armor"
|
||||
"golang.org/x/crypto/cryptobyte"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// stdinInUse is set in main. It's a singleton like os.Stdin.
|
||||
var stdinInUse bool
|
||||
|
||||
type gitHubRecipientError struct {
|
||||
username string
|
||||
}
|
||||
|
||||
func (gitHubRecipientError) Error() string {
|
||||
return `"github:" recipients were removed from the design`
|
||||
}
|
||||
|
||||
func parseRecipient(arg string) (age.Recipient, error) {
|
||||
switch {
|
||||
case strings.HasPrefix(arg, "age1"):
|
||||
@@ -30,8 +41,7 @@ func parseRecipient(arg string) (age.Recipient, error) {
|
||||
return agessh.ParseRecipient(arg)
|
||||
case strings.HasPrefix(arg, "github:"):
|
||||
name := strings.TrimPrefix(arg, "github:")
|
||||
return nil, fmt.Errorf(`"github:" recipients were removed from the design.`+"\n"+
|
||||
"Instead, use recipient files like\n\n curl -O https://github.com/%s.keys\n age -R %s.keys\n\n", name, name)
|
||||
return nil, gitHubRecipientError{name}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unknown recipient type: %q", arg)
|
||||
@@ -72,7 +82,7 @@ func parseRecipientsFile(name string) ([]age.Recipient, error) {
|
||||
if err != nil {
|
||||
if t, ok := sshKeyType(line); ok {
|
||||
// Skip unsupported but valid SSH public keys with a warning.
|
||||
log.Printf("Warning: recipients file %q: ignoring unsupported SSH key of type %q at line %d", name, t, n)
|
||||
warningf("recipients file %q: ignoring unsupported SSH key of type %q at line %d", name, t, n)
|
||||
continue
|
||||
}
|
||||
// Hide the error since it might unintentionally leak the contents
|
||||
@@ -114,8 +124,8 @@ func sshKeyType(s string) (string, bool) {
|
||||
}
|
||||
|
||||
// parseIdentitiesFile parses a file that contains age or SSH keys. It returns
|
||||
// one of *age.X25519Identity, *agessh.RSAIdentity, *agessh.Ed25519Identity, or
|
||||
// *agessh.EncryptedSSHIdentity.
|
||||
// one or more of *age.X25519Identity, *agessh.RSAIdentity, *agessh.Ed25519Identity,
|
||||
// *agessh.EncryptedSSHIdentity, or *EncryptedIdentity.
|
||||
func parseIdentitiesFile(name string) ([]age.Identity, error) {
|
||||
var f *os.File
|
||||
if name == "-" {
|
||||
@@ -134,8 +144,40 @@ func parseIdentitiesFile(name string) ([]age.Identity, error) {
|
||||
}
|
||||
|
||||
b := bufio.NewReader(f)
|
||||
const pemHeader = "-----BEGIN"
|
||||
if peeked, _ := b.Peek(len(pemHeader)); string(peeked) == pemHeader {
|
||||
p, _ := b.Peek(14) // length of "age-encryption" and "-----BEGIN AGE"
|
||||
peeked := string(p)
|
||||
|
||||
switch {
|
||||
// An age encrypted file, plain or armored.
|
||||
case peeked == "age-encryption" || peeked == "-----BEGIN AGE":
|
||||
var r io.Reader = b
|
||||
if peeked == "-----BEGIN AGE" {
|
||||
r = armor.NewReader(r)
|
||||
}
|
||||
const privateKeySizeLimit = 1 << 24 // 16 MiB
|
||||
contents, err := ioutil.ReadAll(io.LimitReader(r, privateKeySizeLimit))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read %q: %v", name, err)
|
||||
}
|
||||
if len(contents) == privateKeySizeLimit {
|
||||
return nil, fmt.Errorf("failed to read %q: file too long", name)
|
||||
}
|
||||
return []age.Identity{&EncryptedIdentity{
|
||||
Contents: contents,
|
||||
Passphrase: func() (string, error) {
|
||||
pass, err := readPassphrase(fmt.Sprintf("Enter passphrase for identity file %q:", name))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not read passphrase: %v", err)
|
||||
}
|
||||
return string(pass), nil
|
||||
},
|
||||
NoMatchWarning: func() {
|
||||
warningf("encrypted identity file %q didn't match file's recipients", name)
|
||||
},
|
||||
}}, nil
|
||||
|
||||
// Another PEM file, possibly an SSH private key.
|
||||
case strings.HasPrefix(peeked, "-----BEGIN"):
|
||||
const privateKeySizeLimit = 1 << 14 // 16 KiB
|
||||
contents, err := ioutil.ReadAll(io.LimitReader(b, privateKeySizeLimit))
|
||||
if err != nil {
|
||||
@@ -145,13 +187,15 @@ func parseIdentitiesFile(name string) ([]age.Identity, error) {
|
||||
return nil, fmt.Errorf("failed to read %q: file too long", name)
|
||||
}
|
||||
return parseSSHIdentity(name, contents)
|
||||
}
|
||||
|
||||
ids, err := age.ParseIdentities(b)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read %q: %v", name, err)
|
||||
// An unencrypted age identity file.
|
||||
default:
|
||||
ids, err := age.ParseIdentities(b)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read %q: %v", name, err)
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func parseSSHIdentity(name string, pemBytes []byte) ([]age.Identity, error) {
|
||||
@@ -165,8 +209,7 @@ func parseSSHIdentity(name string, pemBytes []byte) ([]age.Identity, error) {
|
||||
}
|
||||
}
|
||||
passphrasePrompt := func() ([]byte, error) {
|
||||
fmt.Fprintf(os.Stderr, "Enter passphrase for %q: ", name)
|
||||
pass, err := readPassphrase()
|
||||
pass, err := readPassphrase(fmt.Sprintf("Enter passphrase for %q:", name))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not read passphrase for %q: %v", name, err)
|
||||
}
|
||||
|
||||
5
cmd/age/testdata/fail_bad_hmac.age
vendored
Normal file
5
cmd/age/testdata/fail_bad_hmac.age
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
age-encryption.org/v1
|
||||
-> X25519 i6JOY3uvMdBuEybYbTp3ECFsOPEY/A3lJY1l0Qv2NC4
|
||||
cD7VpfIOchU6ZjAccEjlPCNSOdJvVkxZPSf+7XS1YhY
|
||||
--- 1111111111111111111111111111111111111111111
|
||||
<EFBFBD>-\<5C>P9<50><39>0<1D>hń<68><C584>Tt<54>|:٘<>#&R<>r<EFBFBD> <20><>
|
||||
6
cmd/age/testdata/good_simple.age
vendored
Normal file
6
cmd/age/testdata/good_simple.age
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
age-encryption.org/v1
|
||||
-> X25519 kx2RzHNfNuts0I131KwMCyYclZzKCGMzPUaMkH9J4z4
|
||||
9qEzjtIF4NsLFnxv8EEtCwOQiXj5WHl+HWaDKNeAk+4
|
||||
--- N+7l3M/ofCyzZVlPJ33CTHH8AddF0itK70QV+IIvXXA
|
||||
³]Ú É+zAIÉúçê¸Ç<C2B8>éLüü“ªžà
|
||||
Hˆ%Ñ¥£
|
||||
88
doc/age-keygen.1
Normal file
88
doc/age-keygen.1
Normal file
@@ -0,0 +1,88 @@
|
||||
.\" generated with Ronn/v0.7.3
|
||||
.\" http://github.com/rtomayko/ronn/tree/0.7.3
|
||||
.
|
||||
.TH "AGE\-KEYGEN" "1" "September 2021" "" ""
|
||||
.
|
||||
.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
|
||||
141
doc/age-keygen.1.html
Normal file
141
doc/age-keygen.1.html
Normal file
@@ -0,0 +1,141 @@
|
||||
<!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)'>
|
||||
<title>age-keygen(1) - generate age(1) key pairs</title>
|
||||
<style type='text/css' media='all'>
|
||||
/* style: man */
|
||||
body#manpage {margin:0}
|
||||
.mp {max-width:100ex;padding:0 9ex 1ex 4ex}
|
||||
.mp p,.mp pre,.mp ul,.mp ol,.mp dl {margin:0 0 20px 0}
|
||||
.mp h2 {margin:10px 0 0 0}
|
||||
.mp > p,.mp > pre,.mp > ul,.mp > ol,.mp > dl {margin-left:8ex}
|
||||
.mp h3 {margin:0 0 0 4ex}
|
||||
.mp dt {margin:0;clear:left}
|
||||
.mp dt.flush {float:left;width:8ex}
|
||||
.mp dd {margin:0 0 0 9ex}
|
||||
.mp h1,.mp h2,.mp h3,.mp h4 {clear:left}
|
||||
.mp pre {margin-bottom:20px}
|
||||
.mp pre+h2,.mp pre+h3 {margin-top:22px}
|
||||
.mp h2+pre,.mp h3+pre {margin-top:5px}
|
||||
.mp img {display:block;margin:auto}
|
||||
.mp h1.man-title {display:none}
|
||||
.mp,.mp code,.mp pre,.mp tt,.mp kbd,.mp samp,.mp h3,.mp h4 {font-family:monospace;font-size:14px;line-height:1.42857142857143}
|
||||
.mp h2 {font-size:16px;line-height:1.25}
|
||||
.mp h1 {font-size:20px;line-height:2}
|
||||
.mp {text-align:justify;background:#fff}
|
||||
.mp,.mp code,.mp pre,.mp pre code,.mp tt,.mp kbd,.mp samp {color:#131211}
|
||||
.mp h1,.mp h2,.mp h3,.mp h4 {color:#030201}
|
||||
.mp u {text-decoration:underline}
|
||||
.mp code,.mp strong,.mp b {font-weight:bold;color:#131211}
|
||||
.mp em,.mp var {font-style:italic;color:#232221;text-decoration:none}
|
||||
.mp a,.mp a:link,.mp a:hover,.mp a code,.mp a pre,.mp a tt,.mp a kbd,.mp a samp {color:#0000ff}
|
||||
.mp b.man-ref {font-weight:normal;color:#434241}
|
||||
.mp pre {padding:0 4ex}
|
||||
.mp pre code {font-weight:normal;color:#434241}
|
||||
.mp h2+pre,h3+pre {padding-left:0}
|
||||
ol.man-decor,ol.man-decor li {margin:3px 0 10px 0;padding:0;float:left;width:33%;list-style-type:none;text-transform:uppercase;color:#999;letter-spacing:1px}
|
||||
ol.man-decor {width:100%}
|
||||
ol.man-decor li.tl {text-align:left}
|
||||
ol.man-decor li.tc {text-align:center;letter-spacing:4px}
|
||||
ol.man-decor li.tr {text-align:right;float:right}
|
||||
</style>
|
||||
</head>
|
||||
<!--
|
||||
The following styles are deprecated and will be removed at some point:
|
||||
div#man, div#man ol.man, div#man ol.head, div#man ol.man.
|
||||
|
||||
The .man-page, .man-decor, .man-head, .man-foot, .man-title, and
|
||||
.man-navigation should be used instead.
|
||||
-->
|
||||
<body id='manpage'>
|
||||
<div class='mp' id='man'>
|
||||
|
||||
<div class='man-navigation' style='display:none'>
|
||||
<a href="#NAME">NAME</a>
|
||||
<a href="#SYNOPSIS">SYNOPSIS</a>
|
||||
<a href="#DESCRIPTION">DESCRIPTION</a>
|
||||
<a href="#OPTIONS">OPTIONS</a>
|
||||
<a href="#EXAMPLES">EXAMPLES</a>
|
||||
<a href="#SEE-ALSO">SEE ALSO</a>
|
||||
<a href="#AUTHORS">AUTHORS</a>
|
||||
</div>
|
||||
|
||||
<ol class='man-decor man-head man head'>
|
||||
<li class='tl'>age-keygen(1)</li>
|
||||
<li class='tc'></li>
|
||||
<li class='tr'>age-keygen(1)</li>
|
||||
</ol>
|
||||
|
||||
<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>
|
||||
|
||||
<h2 id="DESCRIPTION">DESCRIPTION</h2>
|
||||
|
||||
<p><code>age-keygen</code> generates a new native <a class="man-ref" href="age.1.html">age<span class="s">(1)</span></a> key pair, and outputs the identity to
|
||||
standard output or to the <var>OUTPUT</var> file. The output includes the public key and
|
||||
the current time as comments.</p>
|
||||
|
||||
<p>If the output is not going to a terminal, <code>age-keygen</code> prints the public key to
|
||||
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>
|
||||
|
||||
<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>
|
||||
</dl>
|
||||
|
||||
|
||||
<h2 id="EXAMPLES">EXAMPLES</h2>
|
||||
|
||||
<p>Generate a new identity:</p>
|
||||
|
||||
<pre><code>$ age-keygen
|
||||
# created: 2021-01-02T15:30:45+01:00
|
||||
# public key: age1lvyvwawkr0mcnnnncaghunadrqkmuf9e6507x9y920xxpp866cnql7dp2z
|
||||
AGE-SECRET-KEY-1N9JEPW6DWJ0ZQUDX63F5A03GX8QUW7PXDE39N8UYF82VZ9PC8UFS3M7XA9
|
||||
</code></pre>
|
||||
|
||||
<p>Write a new identity to <code>key.txt</code>:</p>
|
||||
|
||||
<pre><code>$ age-keygen -o key.txt
|
||||
Public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
|
||||
</code></pre>
|
||||
|
||||
<p>Convert an identity to a recipient:</p>
|
||||
|
||||
<pre><code>$ age-keygen -y key.txt
|
||||
age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
|
||||
</code></pre>
|
||||
|
||||
<h2 id="SEE-ALSO">SEE ALSO</h2>
|
||||
|
||||
<p><a class="man-ref" href="age.1.html">age<span class="s">(1)</span></a></p>
|
||||
|
||||
<h2 id="AUTHORS">AUTHORS</h2>
|
||||
|
||||
<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 2021</li>
|
||||
<li class='tr'>age-keygen(1)</li>
|
||||
</ol>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
57
doc/age-keygen.1.ronn
Normal file
57
doc/age-keygen.1.ronn
Normal file
@@ -0,0 +1,57 @@
|
||||
age-keygen(1) -- generate age(1) key pairs
|
||||
====================================================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`age-keygen` [`-o` <OUTPUT>]<br>
|
||||
`age-keygen` `-y` [`-o` <OUTPUT>] [<INPUT>]<br>
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
`age-keygen` generates a new native age(1) key pair, and outputs the identity to
|
||||
standard output or to the <OUTPUT> file. The output includes the public key and
|
||||
the current time as comments.
|
||||
|
||||
If the output is not going to a terminal, `age-keygen` prints the public key to
|
||||
standard error.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-o`, `--output`=<OUTPUT>:
|
||||
Write the identity to <OUTPUT> instead of standard output.
|
||||
|
||||
If <OUTPUT> already exists, it is not overwritten.
|
||||
|
||||
* `-y`:
|
||||
Read an identity file from <INPUT> or from standard input and output the
|
||||
corresponding recipient(s), one per line, with no comments.
|
||||
|
||||
* `--version`:
|
||||
Print the version and exit.
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
Generate a new identity:
|
||||
|
||||
$ age-keygen
|
||||
# created: 2021-01-02T15:30:45+01:00
|
||||
# public key: age1lvyvwawkr0mcnnnncaghunadrqkmuf9e6507x9y920xxpp866cnql7dp2z
|
||||
AGE-SECRET-KEY-1N9JEPW6DWJ0ZQUDX63F5A03GX8QUW7PXDE39N8UYF82VZ9PC8UFS3M7XA9
|
||||
|
||||
Write a new identity to `key.txt`:
|
||||
|
||||
$ age-keygen -o key.txt
|
||||
Public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
|
||||
|
||||
Convert an identity to a recipient:
|
||||
|
||||
$ age-keygen -y key.txt
|
||||
age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
|
||||
|
||||
## SEE ALSO
|
||||
|
||||
age(1)
|
||||
|
||||
## AUTHORS
|
||||
|
||||
Filippo Valsorda <age@filippo.io>
|
||||
320
doc/age.1
Normal file
320
doc/age.1
Normal file
@@ -0,0 +1,320 @@
|
||||
.\" generated with Ronn/v0.7.3
|
||||
.\" http://github.com/rtomayko/ronn/tree/0.7.3
|
||||
.
|
||||
.TH "AGE" "1" "September 2021" "" ""
|
||||
.
|
||||
.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]
|
||||
.
|
||||
.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\-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\-\-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\-\-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 \fB\-R\fR/\fB\-\-recipients\-file\fR, 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 \fB\-r\fR/\fB\-\-recipient\fR, 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 options can\'t be used with \fB\-r\fR/\fB\-\-recipient\fR or \fB\-R\fR/\fB\-\-recipients\-file\fR\.
|
||||
.
|
||||
.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\.
|
||||
.
|
||||
.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\.
|
||||
.
|
||||
.IP
|
||||
If \fB\-e\fR/\fB\-\-encrypt\fR is explicitly specified (to avoid confusion), \fB\-i\fR/\fB\-\-identity\fR may also be used to encrypt to the \fBRECIPIENTS\fR corresponding to the \fBIDENTITIES\fR listed at \fIPATH\fR\. This allows using an identity file as a symmetric key, if desired\.
|
||||
.
|
||||
.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\.
|
||||
.
|
||||
.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
|
||||
.
|
||||
.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\.
|
||||
.
|
||||
.SH "EXIT STATUS"
|
||||
\fBage\fR will exit 0 if and only if encryption or decryption are succesful for the full length of the input\.
|
||||
.
|
||||
.P
|
||||
If an error occurs during decryption, partial output might still be generated, but only if it was possible to securely authenticate it\. No unauthenticathed output is ever released\.
|
||||
.
|
||||
.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, and 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
|
||||
# Bob
|
||||
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):
|
||||
Using the autogenerated passphrase "hip\-roast\-boring\-snake\-mention\-east\-wasp\-honey\-input\-actress"\.
|
||||
|
||||
$ age \-r age1yhm4gctwfmrpz87tdslm550wrx6m79y9f2hdzt0lndjnehwj0ukqrjpyx5 secrets\.txt > secrets\.txt\.age
|
||||
|
||||
$ age \-d \-i key\.age secrets\.txt\.age > secrets\.txt
|
||||
Enter passphrase for identity file "key\.age":
|
||||
.
|
||||
.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 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
|
||||
347
doc/age.1.html
Normal file
347
doc/age.1.html
Normal file
@@ -0,0 +1,347 @@
|
||||
<!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)'>
|
||||
<title>age(1) - simple, modern, and secure file encryption</title>
|
||||
<style type='text/css' media='all'>
|
||||
/* style: man */
|
||||
body#manpage {margin:0}
|
||||
.mp {max-width:100ex;padding:0 9ex 1ex 4ex}
|
||||
.mp p,.mp pre,.mp ul,.mp ol,.mp dl {margin:0 0 20px 0}
|
||||
.mp h2 {margin:10px 0 0 0}
|
||||
.mp > p,.mp > pre,.mp > ul,.mp > ol,.mp > dl {margin-left:8ex}
|
||||
.mp h3 {margin:0 0 0 4ex}
|
||||
.mp dt {margin:0;clear:left}
|
||||
.mp dt.flush {float:left;width:8ex}
|
||||
.mp dd {margin:0 0 0 9ex}
|
||||
.mp h1,.mp h2,.mp h3,.mp h4 {clear:left}
|
||||
.mp pre {margin-bottom:20px}
|
||||
.mp pre+h2,.mp pre+h3 {margin-top:22px}
|
||||
.mp h2+pre,.mp h3+pre {margin-top:5px}
|
||||
.mp img {display:block;margin:auto}
|
||||
.mp h1.man-title {display:none}
|
||||
.mp,.mp code,.mp pre,.mp tt,.mp kbd,.mp samp,.mp h3,.mp h4 {font-family:monospace;font-size:14px;line-height:1.42857142857143}
|
||||
.mp h2 {font-size:16px;line-height:1.25}
|
||||
.mp h1 {font-size:20px;line-height:2}
|
||||
.mp {text-align:justify;background:#fff}
|
||||
.mp,.mp code,.mp pre,.mp pre code,.mp tt,.mp kbd,.mp samp {color:#131211}
|
||||
.mp h1,.mp h2,.mp h3,.mp h4 {color:#030201}
|
||||
.mp u {text-decoration:underline}
|
||||
.mp code,.mp strong,.mp b {font-weight:bold;color:#131211}
|
||||
.mp em,.mp var {font-style:italic;color:#232221;text-decoration:none}
|
||||
.mp a,.mp a:link,.mp a:hover,.mp a code,.mp a pre,.mp a tt,.mp a kbd,.mp a samp {color:#0000ff}
|
||||
.mp b.man-ref {font-weight:normal;color:#434241}
|
||||
.mp pre {padding:0 4ex}
|
||||
.mp pre code {font-weight:normal;color:#434241}
|
||||
.mp h2+pre,h3+pre {padding-left:0}
|
||||
ol.man-decor,ol.man-decor li {margin:3px 0 10px 0;padding:0;float:left;width:33%;list-style-type:none;text-transform:uppercase;color:#999;letter-spacing:1px}
|
||||
ol.man-decor {width:100%}
|
||||
ol.man-decor li.tl {text-align:left}
|
||||
ol.man-decor li.tc {text-align:center;letter-spacing:4px}
|
||||
ol.man-decor li.tr {text-align:right;float:right}
|
||||
</style>
|
||||
</head>
|
||||
<!--
|
||||
The following styles are deprecated and will be removed at some point:
|
||||
div#man, div#man ol.man, div#man ol.head, div#man ol.man.
|
||||
|
||||
The .man-page, .man-decor, .man-head, .man-foot, .man-title, and
|
||||
.man-navigation should be used instead.
|
||||
-->
|
||||
<body id='manpage'>
|
||||
<div class='mp' id='man'>
|
||||
|
||||
<div class='man-navigation' style='display:none'>
|
||||
<a href="#NAME">NAME</a>
|
||||
<a href="#SYNOPSIS">SYNOPSIS</a>
|
||||
<a href="#DESCRIPTION">DESCRIPTION</a>
|
||||
<a href="#OPTIONS">OPTIONS</a>
|
||||
<a href="#RECIPIENTS-AND-IDENTITIES">RECIPIENTS AND IDENTITIES</a>
|
||||
<a href="#EXIT-STATUS">EXIT STATUS</a>
|
||||
<a href="#BACKWARDS-COMPATIBILITY">BACKWARDS COMPATIBILITY</a>
|
||||
<a href="#EXAMPLES">EXAMPLES</a>
|
||||
<a href="#SEE-ALSO">SEE ALSO</a>
|
||||
<a href="#AUTHORS">AUTHORS</a>
|
||||
</div>
|
||||
|
||||
<ol class='man-decor man-head man head'>
|
||||
<li class='tl'>age(1)</li>
|
||||
<li class='tc'></li>
|
||||
<li class='tr'>age(1)</li>
|
||||
</ol>
|
||||
|
||||
<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>-o</code> <var>OUTPUT</var>] [<var>INPUT</var>]<br /></p>
|
||||
|
||||
<h2 id="DESCRIPTION">DESCRIPTION</h2>
|
||||
|
||||
<p><code>age</code> encrypts or decrypts <var>INPUT</var> to <var>OUTPUT</var>. The <var>INPUT</var> argument is
|
||||
optional and defaults to standard input. Only a single <var>INPUT</var> file may be
|
||||
specified. If <code>-o</code> is not specified, <var>OUTPUT</var> defaults to standard output.</p>
|
||||
|
||||
<p>If <code>--passphrase</code> is specified, the file is encrypted with a passphrase
|
||||
requested interactively. Otherwise, it's encrypted to one or more
|
||||
<a href="#RECIPIENTS-AND-IDENTITIES" title="RECIPIENTS AND IDENTITIES" data-bare-link="true">RECIPIENTS</a> specified with <code>-r</code>/<code>--recipient</code> or
|
||||
<code>-R</code>/<code>--recipients-file</code>. Every recipient can decrypt the file.</p>
|
||||
|
||||
<p>In <code>--decrypt</code> mode, passphrase-encrypted files are detected automatically and
|
||||
the passphrase is requested interactively. Otherwise, one or more
|
||||
<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 to decrypt the file.</p>
|
||||
|
||||
<p><code>age</code> encrypted files are binary and not malleable, with around 200 bytes of
|
||||
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>
|
||||
|
||||
<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>
|
||||
</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>
|
||||
|
||||
<p> This option can be repeated and combined with <code>-R</code>/<code>--recipients-file</code>,
|
||||
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
|
||||
file at <var>PATH</var>, one per line. Empty lines and lines starting with <code>#</code>
|
||||
are ignored as comments.</p>
|
||||
|
||||
<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 <code>-r</code>/<code>--recipient</code>,
|
||||
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 options can't be used with <code>-r</code>/<code>--recipient</code> or
|
||||
<code>-R</code>/<code>--recipients-file</code>.</p></dd>
|
||||
<dt><code>-a</code>, <code>--armor</code></dt><dd><p> Encrypt to an ASCII-only "armored" encoding.</p>
|
||||
|
||||
<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>
|
||||
</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>
|
||||
|
||||
<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> <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.
|
||||
Empty lines and lines starting with "<code>#</code>" are ignored as comments.</p>
|
||||
|
||||
<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.
|
||||
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.
|
||||
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 provided, and the first one matching one of the file's recipients is
|
||||
used. Unused identities are ignored.</p>
|
||||
|
||||
<p> If <code>-e</code>/<code>--encrypt</code> is explicitly specified (to avoid confusion),
|
||||
<code>-i</code>/<code>--identity</code> may also be used to encrypt to the <code>RECIPIENTS</code>
|
||||
corresponding to the <code>IDENTITIES</code> listed at <var>PATH</var>. This allows using an
|
||||
identity file as a symmetric key, if desired.</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
|
||||
to. <code>IDENTITIES</code> are private values, like a private key, that allow decrypting
|
||||
a file encrypted to the corresponding <code>RECIPIENT</code>.</p>
|
||||
|
||||
<h3 id="Native-X25519-keys">Native X25519 keys</h3>
|
||||
|
||||
<p>Native <code>age</code> key pairs are generated with <a class="man-ref" href="age-keygen.1.html">age-keygen<span class="s">(1)</span></a>, and provide small
|
||||
encodings and strong encryption based on X25519. They are the recommended
|
||||
recipient type for most applications.</p>
|
||||
|
||||
<p>A <code>RECIPIENT</code> encoding begins with <code>age1</code> and looks like the following:</p>
|
||||
|
||||
<pre><code>age1gde3ncmahlqd9gg50tanl99r960llztrhfapnmx853s4tjum03uqfssgdh
|
||||
</code></pre>
|
||||
|
||||
<p>An <code>IDENTITY</code> encoding begins with <code>AGE-SECRET-KEY-1</code> and looks like the
|
||||
following:</p>
|
||||
|
||||
<pre><code>AGE-SECRET-KEY-1KTYK6RVLN5TAPE7VF6FQQSKZ9HWWCDSKUGXXNUQDWZ7XXT5YK5LSF3UTKQ
|
||||
</code></pre>
|
||||
|
||||
<p>An encrypted file can't be linked to the native recipient it's encrypted to
|
||||
without access to the corresponding identity.</p>
|
||||
|
||||
<h3 id="SSH-keys">SSH keys</h3>
|
||||
|
||||
<p>As a convenience feature, <code>age</code> also supports encrypting to RSA or Ed25519
|
||||
<span class="man-ref">ssh<span class="s">(1)</span></span> 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>
|
||||
|
||||
<p>A <code>RECIPIENT</code> encoding is an SSH public key in <code>authorized_keys</code> format
|
||||
(see the <code>AUTHORIZED_KEYS FILE FORMAT</code> section of <span class="man-ref">sshd<span class="s">(8)</span></span>), starting with
|
||||
<code>ssh-rsa</code> or <code>ssh-ed25519</code>, like the following:</p>
|
||||
|
||||
<pre><code>ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDULTit0KUehbi[...]GU4BtElAbzh8=
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH9pO5pz22JZEas[...]l1uZc31FGYMXa
|
||||
</code></pre>
|
||||
|
||||
<p>The comment at the end of the line, if present, is ignored.</p>
|
||||
|
||||
<p>In recipient files passed to <code>-R</code>/<code>--recipients-file</code>, unsupported but valid
|
||||
SSH public keys are ignored with a warning, to facilitate using
|
||||
<code>authorized_keys</code> or GitHub <code>.keys</code> files. (See <a href="#EXAMPLES" title="EXAMPLES" data-bare-link="true">EXAMPLES</a>.)</p>
|
||||
|
||||
<p>An <code>IDENTITY</code> is an SSH private key <em>file</em> passed individually to
|
||||
<code>-i</code>/<code>--identity</code>. Note that keys held on hardware tokens such as YubiKeys
|
||||
or accessed via <span class="man-ref">ssh-agent<span class="s">(1)</span></span> are not supported.</p>
|
||||
|
||||
<p>An encrypted file <em>can</em> be linked to the SSH public key it was encrypted to.
|
||||
This is so that <code>age</code> can identify the correct SSH private key before
|
||||
requesting its password, if any.</p>
|
||||
|
||||
<h2 id="EXIT-STATUS">EXIT STATUS</h2>
|
||||
|
||||
<p><code>age</code> will exit 0 if and only if encryption or decryption are succesful for the
|
||||
full length of the input.</p>
|
||||
|
||||
<p>If an error occurs during decryption, partial output might still be generated,
|
||||
but only if it was possible to securely authenticate it. No unauthenticathed
|
||||
output is ever released.</p>
|
||||
|
||||
<h2 id="BACKWARDS-COMPATIBILITY">BACKWARDS COMPATIBILITY</h2>
|
||||
|
||||
<p>Files encrypted with a stable version (not alpha, beta, or release candidate) of
|
||||
<code>age</code>, or with any v1.0.0 beta or release candidate, will decrypt with any later
|
||||
version of the tool.</p>
|
||||
|
||||
<p>If decrypting older files poses a security risk, doing so might cause an error
|
||||
by default, and a flag will be provided to force the operation.</p>
|
||||
|
||||
<h2 id="EXAMPLES">EXAMPLES</h2>
|
||||
|
||||
<p>Generate a new identity, encrypt data, and decrypt:</p>
|
||||
|
||||
<pre><code>$ 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
|
||||
</code></pre>
|
||||
|
||||
<p>Encrypt <code>example.jpg</code> to multiple recipients and output to <code>example.jpg.age</code>:</p>
|
||||
|
||||
<pre><code>$ age -o example.jpg.age -r age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p \
|
||||
-r age1lggyhqrw2nlhcxprm67z43rta597azn8gknawjehu9d9dl0jq3yqqvfafg example.jpg
|
||||
</code></pre>
|
||||
|
||||
<p>Encrypt to a list of recipients:</p>
|
||||
|
||||
<pre><code>$ cat > recipients.txt
|
||||
# Alice
|
||||
age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
|
||||
# Bob
|
||||
age1lggyhqrw2nlhcxprm67z43rta597azn8gknawjehu9d9dl0jq3yqqvfafg
|
||||
|
||||
$ age -R recipients.txt example.jpg > example.jpg.age
|
||||
</code></pre>
|
||||
|
||||
<p>Encrypt and decrypt a file using a passphrase:</p>
|
||||
|
||||
<pre><code>$ 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:
|
||||
</code></pre>
|
||||
|
||||
<p>Encrypt and decrypt with a passphrase-protected identity file:</p>
|
||||
|
||||
<pre><code>$ age-keygen | age -p > key.age
|
||||
Public key: age1yhm4gctwfmrpz87tdslm550wrx6m79y9f2hdzt0lndjnehwj0ukqrjpyx5
|
||||
Enter passphrase (leave empty to autogenerate a secure one):
|
||||
Using the autogenerated passphrase "hip-roast-boring-snake-mention-east-wasp-honey-input-actress".
|
||||
|
||||
$ age -r age1yhm4gctwfmrpz87tdslm550wrx6m79y9f2hdzt0lndjnehwj0ukqrjpyx5 secrets.txt > secrets.txt.age
|
||||
|
||||
$ age -d -i key.age secrets.txt.age > secrets.txt
|
||||
Enter passphrase for identity file "key.age":
|
||||
</code></pre>
|
||||
|
||||
<p>Encrypt and decrypt with an SSH public key:</p>
|
||||
|
||||
<pre><code>$ age -R ~/.ssh/id_ed25519.pub example.jpg > example.jpg.age
|
||||
|
||||
$ age -d -i ~/.ssh/id_ed25519 example.jpg.age > example.jpg
|
||||
</code></pre>
|
||||
|
||||
<p>Encrypt to the SSH keys of a GitHub user:</p>
|
||||
|
||||
<pre><code>$ curl https://github.com/benjojo.keys | age -R - example.jpg > example.jpg.age
|
||||
</code></pre>
|
||||
|
||||
<h2 id="SEE-ALSO">SEE ALSO</h2>
|
||||
|
||||
<p><a class="man-ref" href="age-keygen.1.html">age-keygen<span class="s">(1)</span></a></p>
|
||||
|
||||
<h2 id="AUTHORS">AUTHORS</h2>
|
||||
|
||||
<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 2021</li>
|
||||
<li class='tr'>age(1)</li>
|
||||
</ol>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
257
doc/age.1.ronn
Normal file
257
doc/age.1.ronn
Normal file
@@ -0,0 +1,257 @@
|
||||
age(1) -- simple, modern, and secure file encryption
|
||||
====================================================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`age` [`--encrypt`] (`-r` <RECIPIENT> | `-R` <PATH>)... [`--armor`] [`-o` <OUTPUT>] [<INPUT>]<br>
|
||||
`age` [`--encrypt`] `--passphrase` [`--armor`] [`-o` <OUTPUT>] [<INPUT>]<br>
|
||||
`age` `--decrypt` [`-i` <PATH>]... [`-o` <OUTPUT>] [<INPUT>]<br>
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
`age` encrypts or decrypts <INPUT> to <OUTPUT>. The <INPUT> argument is
|
||||
optional and defaults to standard input. Only a single <INPUT> file may be
|
||||
specified. If `-o` is not specified, <OUTPUT> defaults to standard output.
|
||||
|
||||
If `--passphrase` is specified, the file is encrypted with a passphrase
|
||||
requested interactively. Otherwise, it's encrypted to one or more
|
||||
[RECIPIENTS][RECIPIENTS AND IDENTITIES] specified with `-r`/`--recipient` or
|
||||
`-R`/`--recipients-file`. Every recipient can decrypt the file.
|
||||
|
||||
In `--decrypt` mode, passphrase-encrypted files are detected automatically and
|
||||
the passphrase is requested interactively. Otherwise, one or more
|
||||
[IDENTITIES][RECIPIENTS AND IDENTITIES] specified with `-i`/`--identity` are
|
||||
used to decrypt the file.
|
||||
|
||||
`age` encrypted files are binary and not malleable, with around 200 bytes of
|
||||
overhead per recipient, plus 16 bytes every 64KiB of plaintext.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-o`, `--output`=<OUTPUT>:
|
||||
Write encrypted or decrypted file to <OUTPUT> instead of standard output.
|
||||
If <OUTPUT> already exists it will be overwritten.
|
||||
|
||||
If encrypting without `--armor`, `age` will refuse to output binary to a
|
||||
TTY. This can be forced by specifying `-` as <OUTPUT>.
|
||||
|
||||
* `--version`:
|
||||
Print the version and exit.
|
||||
|
||||
### Encryption options
|
||||
|
||||
* `-e`, `--encrypt`:
|
||||
Encrypt <INPUT> to <OUTPUT>. This is the default.
|
||||
|
||||
* `-r`, `--recipient`=<RECIPIENT>:
|
||||
Encrypt to the explicitly specified <RECIPIENT>. See the
|
||||
[RECIPIENTS AND IDENTITIES][] section for possible recipient formats.
|
||||
|
||||
This option can be repeated and combined with `-R`/`--recipients-file`,
|
||||
and the file can be decrypted by all provided recipients independently.
|
||||
|
||||
* `-R`, `--recipients-file`=<PATH>:
|
||||
Encrypt to the [RECIPIENTS][RECIPIENTS AND IDENTITIES] listed in the
|
||||
file at <PATH>, one per line. Empty lines and lines starting with `#`
|
||||
are ignored as comments.
|
||||
|
||||
If <PATH> is `-`, the recipients are read from standard input. In
|
||||
this case, the <INPUT> argument must be specified.
|
||||
|
||||
This option can be repeated and combined with `-r`/`--recipient`,
|
||||
and the file can be decrypted by all provided recipients independently.
|
||||
|
||||
* `-p`, `--passphrase`:
|
||||
Encrypt with a passphrase, requested interactively from the terminal.
|
||||
`age` will offer to auto-generate a secure passphrase.
|
||||
|
||||
This options can't be used with `-r`/`--recipient` or
|
||||
`-R`/`--recipients-file`.
|
||||
|
||||
* `-a`, `--armor`:
|
||||
Encrypt to an ASCII-only "armored" encoding.
|
||||
|
||||
`age` armor is a strict version of PEM with type `AGE ENCRYPTED FILE`,
|
||||
canonical "strict" Base64, no headers, and no support for leading and
|
||||
trailing extra data.
|
||||
|
||||
Decryption transparently detects and decodes ASCII armoring.
|
||||
|
||||
### Decryption options
|
||||
|
||||
* `-d`, `--decrypt`:
|
||||
Decrypt <INPUT> to <OUTPUT>.
|
||||
|
||||
If <INPUT> is passphrase encrypted, it will be automatically detected
|
||||
and the passphrase will be requested interactively. Otherwise, the
|
||||
[IDENTITIES][RECIPIENTS AND IDENTITIES] specified with `-i`/`--identity`
|
||||
are used.
|
||||
|
||||
ASCII armoring is transparently detected and decoded.
|
||||
|
||||
* `-i`, `--identity`=<PATH>:
|
||||
Decrypt using the [IDENTITIES][RECIPIENTS AND IDENTITIES] at <PATH>.
|
||||
|
||||
<PATH> may be one of the following:
|
||||
|
||||
a\. A file listing [IDENTITIES][RECIPIENTS AND IDENTITIES] one per line.
|
||||
Empty lines and lines starting with "`#`" are ignored as comments.
|
||||
|
||||
b\. A passphrase encrypted age file, containing
|
||||
[IDENTITIES][RECIPIENTS AND IDENTITIES] 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.
|
||||
|
||||
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
|
||||
[SSH keys][] section for more information, including supported key types.
|
||||
|
||||
d\. "`-`", causing one of the options above to be read from standard input.
|
||||
In this case, the <INPUT> argument must be specified.
|
||||
|
||||
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.
|
||||
|
||||
If `-e`/`--encrypt` is explicitly specified (to avoid confusion),
|
||||
`-i`/`--identity` may also be used to encrypt to the `RECIPIENTS`
|
||||
corresponding to the `IDENTITIES` listed at <PATH>. This allows using an
|
||||
identity file as a symmetric key, if desired.
|
||||
|
||||
## RECIPIENTS AND IDENTITIES
|
||||
|
||||
`RECIPIENTS` are public values, like a public key, that a file can be encrypted
|
||||
to. `IDENTITIES` are private values, like a private key, that allow decrypting
|
||||
a file encrypted to the corresponding `RECIPIENT`.
|
||||
|
||||
### Native X25519 keys
|
||||
|
||||
Native `age` 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.
|
||||
|
||||
A `RECIPIENT` encoding begins with `age1` and looks like the following:
|
||||
|
||||
age1gde3ncmahlqd9gg50tanl99r960llztrhfapnmx853s4tjum03uqfssgdh
|
||||
|
||||
An `IDENTITY` encoding begins with `AGE-SECRET-KEY-1` and looks like the
|
||||
following:
|
||||
|
||||
AGE-SECRET-KEY-1KTYK6RVLN5TAPE7VF6FQQSKZ9HWWCDSKUGXXNUQDWZ7XXT5YK5LSF3UTKQ
|
||||
|
||||
An encrypted file can't be linked to the native recipient it's encrypted to
|
||||
without access to the corresponding identity.
|
||||
|
||||
### SSH keys
|
||||
|
||||
As a convenience feature, `age` 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.
|
||||
|
||||
A `RECIPIENT` encoding is an SSH public key in `authorized_keys` format
|
||||
(see the `AUTHORIZED_KEYS FILE FORMAT` section of sshd(8)), starting with
|
||||
`ssh-rsa` or `ssh-ed25519`, like the following:
|
||||
|
||||
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDULTit0KUehbi[...]GU4BtElAbzh8=
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH9pO5pz22JZEas[...]l1uZc31FGYMXa
|
||||
|
||||
The comment at the end of the line, if present, is ignored.
|
||||
|
||||
In recipient files passed to `-R`/`--recipients-file`, unsupported but valid
|
||||
SSH public keys are ignored with a warning, to facilitate using
|
||||
`authorized_keys` or GitHub `.keys` files. (See [EXAMPLES][].)
|
||||
|
||||
An `IDENTITY` is an SSH private key _file_ passed individually to
|
||||
`-i`/`--identity`. Note that keys held on hardware tokens such as YubiKeys
|
||||
or accessed via ssh-agent(1) are not supported.
|
||||
|
||||
An encrypted file _can_ be linked to the SSH public key it was encrypted to.
|
||||
This is so that `age` can identify the correct SSH private key before
|
||||
requesting its password, if any.
|
||||
|
||||
## EXIT STATUS
|
||||
|
||||
`age` will exit 0 if and only if encryption or decryption are succesful for the
|
||||
full length of the input.
|
||||
|
||||
If an error occurs during decryption, partial output might still be generated,
|
||||
but only if it was possible to securely authenticate it. No unauthenticathed
|
||||
output is ever released.
|
||||
|
||||
## BACKWARDS COMPATIBILITY
|
||||
|
||||
Files encrypted with a stable version (not alpha, beta, or release candidate) of
|
||||
`age`, or with any v1.0.0 beta or release candidate, will decrypt with any later
|
||||
version of the tool.
|
||||
|
||||
If decrypting older files poses a security risk, doing so might cause an error
|
||||
by default, and a flag will be provided to force the operation.
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
Generate a new identity, encrypt data, and decrypt:
|
||||
|
||||
$ 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
|
||||
|
||||
Encrypt `example.jpg` to multiple recipients and output to `example.jpg.age`:
|
||||
|
||||
$ age -o example.jpg.age -r age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p \
|
||||
-r age1lggyhqrw2nlhcxprm67z43rta597azn8gknawjehu9d9dl0jq3yqqvfafg example.jpg
|
||||
|
||||
Encrypt to a list of recipients:
|
||||
|
||||
$ cat > recipients.txt
|
||||
# Alice
|
||||
age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
|
||||
# Bob
|
||||
age1lggyhqrw2nlhcxprm67z43rta597azn8gknawjehu9d9dl0jq3yqqvfafg
|
||||
|
||||
$ age -R recipients.txt example.jpg > example.jpg.age
|
||||
|
||||
Encrypt and decrypt a file using a passphrase:
|
||||
|
||||
$ 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:
|
||||
|
||||
Encrypt and decrypt with a passphrase-protected identity file:
|
||||
|
||||
$ age-keygen | age -p > key.age
|
||||
Public key: age1yhm4gctwfmrpz87tdslm550wrx6m79y9f2hdzt0lndjnehwj0ukqrjpyx5
|
||||
Enter passphrase (leave empty to autogenerate a secure one):
|
||||
Using the autogenerated passphrase "hip-roast-boring-snake-mention-east-wasp-honey-input-actress".
|
||||
|
||||
$ age -r age1yhm4gctwfmrpz87tdslm550wrx6m79y9f2hdzt0lndjnehwj0ukqrjpyx5 secrets.txt > secrets.txt.age
|
||||
|
||||
$ age -d -i key.age secrets.txt.age > secrets.txt
|
||||
Enter passphrase for identity file "key.age":
|
||||
|
||||
Encrypt and decrypt with an SSH public key:
|
||||
|
||||
$ age -R ~/.ssh/id_ed25519.pub example.jpg > example.jpg.age
|
||||
|
||||
$ age -d -i ~/.ssh/id_ed25519 example.jpg.age > example.jpg
|
||||
|
||||
Encrypt to the SSH keys of a GitHub user:
|
||||
|
||||
$ curl https://github.com/benjojo.keys | age -R - example.jpg > example.jpg.age
|
||||
|
||||
## SEE ALSO
|
||||
|
||||
age-keygen(1)
|
||||
|
||||
## AUTHORS
|
||||
|
||||
Filippo Valsorda <age@filippo.io>
|
||||
10
go.mod
10
go.mod
@@ -1,5 +1,11 @@
|
||||
module filippo.io/age
|
||||
|
||||
go 1.13
|
||||
go 1.17
|
||||
|
||||
require golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad
|
||||
require (
|
||||
filippo.io/edwards25519 v1.0.0-rc.1
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5
|
||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b
|
||||
)
|
||||
|
||||
require golang.org/x/sys v0.0.0-20210903071746-97244b99971b // indirect
|
||||
|
||||
24
go.sum
24
go.sum
@@ -1,10 +1,14 @@
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY=
|
||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221 h1:/ZHdbVpdR/jk3g30/d4yUL0JU9kksj8+F/bnQUVLGDM=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU=
|
||||
filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ=
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210903071746-97244b99971b h1:3Dq0eVHn0uaQJmPO+/aYPI/fRMqdrVDbu7MQcku54gg=
|
||||
golang.org/x/sys v0.0.0-20210903071746-97244b99971b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE=
|
||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
|
||||
@@ -43,25 +43,40 @@ func DecodeString(s string) ([]byte, error) {
|
||||
var EncodeToString = b64.EncodeToString
|
||||
|
||||
const ColumnsPerLine = 64
|
||||
|
||||
const BytesPerLine = ColumnsPerLine / 4 * 3
|
||||
|
||||
// NewlineWriter returns a Writer that writes to dst, inserting an LF character
|
||||
// every ColumnsPerLine bytes. It does not insert a newline neither at the
|
||||
// beginning nor at the end of the stream, but it ensures the last line is
|
||||
// shorter than ColumnsPerLine, which means it might be empty.
|
||||
func NewlineWriter(dst io.Writer) io.Writer {
|
||||
return &newlineWriter{dst: dst}
|
||||
// NewWrappedBase64Encoder returns a WrappedBase64Encoder that writes to dst.
|
||||
func NewWrappedBase64Encoder(enc *base64.Encoding, dst io.Writer) *WrappedBase64Encoder {
|
||||
w := &WrappedBase64Encoder{dst: dst}
|
||||
w.enc = base64.NewEncoder(enc, WriterFunc(w.writeWrapped))
|
||||
return w
|
||||
}
|
||||
|
||||
type newlineWriter struct {
|
||||
type WriterFunc func(p []byte) (int, error)
|
||||
|
||||
func (f WriterFunc) Write(p []byte) (int, error) { return f(p) }
|
||||
|
||||
// WrappedBase64Encoder is a standard base64 encoder that inserts an LF
|
||||
// character every ColumnsPerLine bytes. It does not insert a newline neither at
|
||||
// the beginning nor at the end of the stream, but it ensures the last line is
|
||||
// shorter than ColumnsPerLine, which means it might be empty.
|
||||
type WrappedBase64Encoder struct {
|
||||
enc io.WriteCloser
|
||||
dst io.Writer
|
||||
written int
|
||||
buf bytes.Buffer
|
||||
}
|
||||
|
||||
func (w *newlineWriter) Write(p []byte) (int, error) {
|
||||
func (w *WrappedBase64Encoder) Write(p []byte) (int, error) { return w.enc.Write(p) }
|
||||
|
||||
func (w *WrappedBase64Encoder) Close() error {
|
||||
return w.enc.Close()
|
||||
}
|
||||
|
||||
func (w *WrappedBase64Encoder) writeWrapped(p []byte) (int, error) {
|
||||
if w.buf.Len() != 0 {
|
||||
panic("age: internal error: non-empty newlineWriter.buf")
|
||||
panic("age: internal error: non-empty WrappedBase64Encoder.buf")
|
||||
}
|
||||
for len(p) > 0 {
|
||||
toWrite := ColumnsPerLine - (w.written % ColumnsPerLine)
|
||||
@@ -84,9 +99,18 @@ func (w *newlineWriter) Write(p []byte) (int, error) {
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// LastLineIsEmpty returns whether the last output line was empty, either
|
||||
// because no input was written, or because a multiple of BytesPerLine was.
|
||||
//
|
||||
// Calling LastLineIsEmpty before Close is meaningless.
|
||||
func (w *WrappedBase64Encoder) LastLineIsEmpty() bool {
|
||||
return w.written%ColumnsPerLine == 0
|
||||
}
|
||||
|
||||
const intro = "age-encryption.org/v1\n"
|
||||
|
||||
var recipientPrefix = []byte("->")
|
||||
|
||||
var footerPrefix = []byte("---")
|
||||
|
||||
func (r *Stanza) Marshal(w io.Writer) error {
|
||||
@@ -101,7 +125,7 @@ func (r *Stanza) Marshal(w io.Writer) error {
|
||||
if _, err := io.WriteString(w, "\n"); err != nil {
|
||||
return err
|
||||
}
|
||||
ww := base64.NewEncoder(b64, NewlineWriter(w))
|
||||
ww := NewWrappedBase64Encoder(b64, w)
|
||||
if _, err := ww.Write(r.Body); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -122,6 +122,11 @@ func (i *ScryptIdentity) SetMaxWorkFactor(logN int) {
|
||||
}
|
||||
|
||||
func (i *ScryptIdentity) Unwrap(stanzas []*Stanza) ([]byte, error) {
|
||||
for _, s := range stanzas {
|
||||
if s.Type == "scrypt" && len(stanzas) != 1 {
|
||||
return nil, errors.New("an scrypt recipient must be the only one")
|
||||
}
|
||||
}
|
||||
return multiUnwrap(i.unwrap, stanzas)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user