20 Commits
Author SHA1 Message Date
Filippo Valsorda eaf2cef49d cmd/age: allow reading both passphrase and input from a terminal
Fixes #196
Closes #258
2021-12-07 17:14:47 +01:00
GitHub Actions 581cff8473 doc: regenerate groff and html man pages 2021-10-15 13:09:48 +00:00
Filippo Valsorda 40ef1b6a62 doc: add age(1) and age-keygen(1) man pages
doc: SEC 1 encoding is for ECDSA, which we don't support
doc: fix typo in age-keygen(1) (#273) <Andreas Wachowski>
doc: document backwards compatibility policy
doc: clarify backwards compatibility section
doc: fix typo in age(1) (#333) <y-yagi>
doc: fix typo in age(1) (#336) <puenka>
2021-10-15 09:06:19 -04:00
Ryan Castellucci 11659e8c97 cmd/age-keygen: don't warn about world-readable output for public keys (#268)
Fixes #267
2021-09-07 11:08:37 +02:00
Filippo Valsorda 7309913372 armor: don't leave an empty line before the footer
Closes #264
Fixes #263
2021-09-07 11:01:06 +02:00
Filippo Valsorda 6f86a7f520 agessh: reject small ssh-rsa keys
Fixes #266
2021-09-07 11:01:06 +02:00
Filippo Valsorda b59a9ecb5d age: remove recipient limit
Fixes #139
2021-09-04 15:26:31 +02:00
Filippo Valsorda bceb0e0423 cmd/age,cmd/age-keygen: check Close() error on output files
Fixes #81
2021-09-04 15:25:41 +02:00
Filippo Valsorda 53f0ebda67 cmd/age: improve a couple error messages 2021-03-10 05:38:47 -05:00
Filippo Valsorda f3fdc33c9e cmd/age-keygen: add -y mode to convert identity file to recipients
Copied -y from ssh-keygen. Copied the INPUT as only optional argument
from cmd/age.

Fixes #122
Closes #146
2021-03-10 05:38:47 -05:00
Filippo Valsorda a81357c237 all: update golang.org/x/crypto
Picked the latest version that is packaged in Debian Bullseye.

Fixes #184
2021-03-10 05:38:47 -05:00
Filippo Valsorda 69e2222921 internal/format: drop go-fuzz test case
We were not running it in CI, so it had rotted or was going to rot.
We'll replace it with a native fuzz test once that's ready.
2021-03-10 05:38:47 -05:00
Filippo Valsorda 732f3e8a94 cmd/age: add -e and support encrypting with -i
This will come in handy for symmetric plugins, but make it require an
explicit -e so that missing a -d can't cause a mistaken encryption.
2021-03-10 05:38:47 -05:00
Filippo Valsorda 801a7e8b33 cmd/age: overwrite output file if existing
Not really the safest UX, but age is a UNIX tool, and this is what all
UNIX tools do, so adopt the principle of least surprise.
2021-03-10 05:38:47 -05:00
Filippo Valsorda 629b0dbbc9 Revert "agessh: use filippo.io/edwards25519 for Ed25519 to Curve25519 conversion"
The extra dependency makes it harder to package age. Temporarily drop it
to facilitate getting v1.0.0 into distributions.

This reverts commit 53ccaf8b71.
2021-03-10 05:38:47 -05:00
Filippo Valsorda 710644eef8 Revert "cmd/age: automatically load default SSH key paths"
It's not clear the convenience for SSH keys is worth having any
implicitly configured identity at all. Will revisit after v1.0.0.

This reverts commit 225044b061.
2021-03-10 05:38:47 -05:00
Filippo Valsorda 6c8d072dd8 .github/workflows: add Go tip tests and bump tests to Go 1.16 2021-03-09 18:51:05 -05:00
Filippo Valsorda 225044b061 cmd/age: automatically load default SSH key paths 2021-02-08 19:55:28 +01:00
Filippo Valsorda 6da7d26b4d all: add .gitattributes to protect .age files from autocrlf
It looks like a short message encrypted to a long RSA key is enough
ASCII that it gets autocrlf'd on Windows.
2021-02-08 19:55:28 +01:00
Filippo Valsorda 19e87b75b7 cmd/age: expand test vectors suite 2021-02-08 19:55:28 +01:00
53 changed files with 1679 additions and 207 deletions

No files matched your search

+1
View File
@@ -0,0 +1 @@
*.age binary
+32
View File
@@ -0,0 +1,32 @@
on: [push, pull_request]
name: Go tip tests
jobs:
test:
name: Test
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Install Go tip (UNIX)
if: runner.os != 'Windows'
run: |
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:
fetch-depth: 0
- run: go version
- name: Run tests
run: go test -race ./...
+33
View 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
View 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
View File
@@ -0,0 +1,5 @@
# frozen_string_literal: true
source "https://rubygems.org"
gem "ronn", "~> 0.7.3"
+20
View 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
View File
@@ -0,0 +1,4 @@
name: Ronn
runs:
using: docker
image: Dockerfile
+3 -3
View File
@@ -6,7 +6,7 @@ jobs:
strategy:
fail-fast: false
matrix:
go: [1.14.x, 1.x]
go: [1.15.x, 1.16.x]
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
@@ -16,7 +16,7 @@ jobs:
go-version: ${{ matrix.go }}
- name: Checkout repository
uses: actions/checkout@v2
# - name: Run analyses
# run: go run analysis.go ./...
with:
fetch-depth: 0
- name: Run tests
run: go test -race ./...
+2
View File
@@ -16,5 +16,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
+10 -3
View File
@@ -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 (
@@ -168,9 +178,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 {
+47 -7
View File
@@ -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
}
@@ -102,6 +105,13 @@ func NewRSAIdentity(key *rsa.PrivateKey) (*RSAIdentity, error) {
return i, nil
}
func (i *RSAIdentity) Recipient() age.Recipient {
return &RSARecipient{
sshKey: i.sshKey,
pubKey: &i.k.PublicKey,
}
}
func (i *RSAIdentity) Unwrap(stanzas []*age.Stanza) ([]byte, error) {
return multiUnwrap(i.unwrap, stanzas)
}
@@ -179,14 +189,37 @@ 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) {
// See https://blog.filippo.io/using-ed25519-keys-for-encryption and
// https://pkg.go.dev/filippo.io/edwards25519#Point.BytesMontgomery.
p, err := (&edwards25519.Point{}).SetBytes(pk)
if err != nil {
return nil, err
// 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
}
return p.BytesMontgomery(), nil
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
}
const ed25519Label = "age-encryption.org/v1/ssh-ed25519"
@@ -280,6 +313,13 @@ func ed25519PrivateKeyToCurve25519(pk ed25519.PrivateKey) []byte {
return out[:curve25519.ScalarSize]
}
func (i *Ed25519Identity) Recipient() age.Recipient {
return &Ed25519Recipient{
sshKey: i.sshKey,
theirPublicKey: i.ourPublicKey,
}
}
func (i *Ed25519Identity) Unwrap(stanzas []*age.Stanza) ([]byte, error) {
return multiUnwrap(i.unwrap, stanzas)
}
+12 -1
View File
@@ -11,6 +11,7 @@ import (
"crypto/ed25519"
"crypto/rand"
"crypto/rsa"
"reflect"
"testing"
"filippo.io/age/agessh"
@@ -18,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)
}
@@ -36,6 +37,11 @@ func TestSSHRSARoundTrip(t *testing.T) {
t.Fatal(err)
}
// TODO: replace this with (and go-diff) with go-cmp.
if !reflect.DeepEqual(r, i.Recipient()) {
t.Fatalf("i.Recipient is different from r")
}
fileKey := make([]byte, 16)
if _, err := rand.Read(fileKey); err != nil {
t.Fatal(err)
@@ -74,6 +80,11 @@ func TestSSHEd25519RoundTrip(t *testing.T) {
t.Fatal(err)
}
// TODO: replace this with (and go-diff) with go-cmp.
if !reflect.DeepEqual(r, i.Recipient()) {
t.Fatalf("i.Recipient is different from r")
}
fileKey := make([]byte, 16)
if _, err := rand.Read(fileKey); err != nil {
t.Fatal(err)
+4
View File
@@ -55,6 +55,10 @@ func NewEncryptedSSHIdentity(pubKey ssh.PublicKey, pemBytes []byte, passphrase f
var _ age.Identity = &EncryptedSSHIdentity{}
func (i *EncryptedSSHIdentity) Recipient() (age.Recipient, error) {
return ParseRecipient(string(ssh.MarshalAuthorizedKey(i.pubKey)))
}
// Unwrap implements age.Identity. If the private key is still encrypted, and
// any of the stanzas match the public key, it will request the passphrase. The
// decrypted private key will be cached after the first successful invocation.
+10 -5
View File
@@ -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 {
+18 -1
View File
@@ -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)
+57 -11
View File
@@ -9,6 +9,7 @@ package main
import (
"flag"
"fmt"
"io"
"log"
"os"
"runtime/debug"
@@ -20,16 +21,22 @@ import (
const usage = `Usage:
age-keygen [-o OUTPUT]
age-keygen -y [-o OUTPUT] [INPUT]
Options:
-o, --output OUTPUT Write the key to the file at path OUTPUT.
-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.
If OUTPUT already exists, it is not overwritten.
In -y mode, age-keygen reads an identity file from INPUT or from standard
input and writes the corresponding recipient(s) to OUTPUT or to standard
output, one per line, with no comments.
Examples:
$ age-keygen
@@ -38,7 +45,10 @@ Examples:
AGE-SECRET-KEY-1N9JEPW6DWJ0ZQUDX63F5A03GX8QUW7PXDE39N8UYF82VZ9PC8UFS3M7XA9
$ age-keygen -o key.txt
Public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p`
Public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
$ age-keygen -y key.txt
age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p`
// Version can be set at link time to override debug.BuildInfo.Main.Version,
// which is "(devel)" when building from within the module. See
@@ -50,17 +60,21 @@ func main() {
flag.Usage = func() { fmt.Fprintf(os.Stderr, "%s\n", usage) }
var (
versionFlag bool
outFlag string
versionFlag, convertFlag bool
outFlag string
)
flag.BoolVar(&versionFlag, "version", false, "print the version")
flag.BoolVar(&convertFlag, "y", false, "convert identities to recipients")
flag.StringVar(&outFlag, "o", "", "output to `FILE` (default stdout)")
flag.StringVar(&outFlag, "output", "", "output to `FILE` (default stdout)")
flag.Parse()
if len(flag.Args()) != 0 {
if len(flag.Args()) != 0 && !convertFlag {
log.Fatalf("age-keygen takes no arguments")
}
if len(flag.Args()) > 1 && convertFlag {
log.Fatalf("Too many arguments")
}
if versionFlag {
if Version != "" {
fmt.Println(Version)
@@ -80,17 +94,32 @@ func main() {
if err != nil {
log.Fatalf("Failed to open output file %q: %v", outFlag, err)
}
defer f.Close()
defer func() {
if err := f.Close(); err != nil {
log.Fatalf("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)
}
defer f.Close()
in = f
}
generate(out)
if convertFlag {
convert(in, out)
} else {
if fi, err := out.Stat(); err == nil && fi.Mode().IsRegular() && fi.Mode().Perm()&0004 != 0 {
fmt.Fprintf(os.Stderr, "Warning: writing secret key to a world-readable file.\n")
}
generate(out)
}
}
func generate(out *os.File) {
@@ -107,3 +136,20 @@ func generate(out *os.File) {
fmt.Fprintf(out, "# public key: %s\n", k.Recipient())
fmt.Fprintf(out, "%s\n", k)
}
func convert(in io.Reader, out io.Writer) {
ids, err := age.ParseIdentities(in)
if err != nil {
log.Fatalf("Failed to parse input: %v", err)
}
if len(ids) == 0 {
log.Fatalf("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)
}
fmt.Fprintf(out, "%s\n", id.Recipient())
}
}
+63 -24
View File
@@ -18,6 +18,7 @@ import (
"strings"
"filippo.io/age"
"filippo.io/age/agessh"
"filippo.io/age/armor"
"golang.org/x/crypto/ssh/terminal"
)
@@ -32,20 +33,22 @@ func (f *multiFlag) Set(value string) error {
}
const usage = `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...").
@@ -59,6 +62,9 @@ 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
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.
Example:
$ age-keygen -o key.txt
Public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
@@ -80,16 +86,18 @@ func main() {
}
var (
outFlag string
decryptFlag, armorFlag bool
passFlag, versionFlag bool
recipientFlags, identityFlags multiFlag
recipientsFileFlags multiFlag
outFlag string
decryptFlag, encryptFlag bool
passFlag, versionFlag, armorFlag bool
recipientFlags, identityFlags multiFlag
recipientsFileFlags multiFlag
)
flag.BoolVar(&versionFlag, "version", false, "print the version")
flag.BoolVar(&decryptFlag, "d", false, "decrypt the input")
flag.BoolVar(&decryptFlag, "decrypt", false, "decrypt the input")
flag.BoolVar(&encryptFlag, "e", false, "encrypt the input")
flag.BoolVar(&encryptFlag, "encrypt", false, "encrypt the input")
flag.BoolVar(&passFlag, "p", false, "use a passphrase")
flag.BoolVar(&passFlag, "passphrase", false, "use a passphrase")
flag.StringVar(&outFlag, "o", "", "output to `FILE` (default stdout)")
@@ -118,11 +126,14 @@ func main() {
}
if flag.NArg() > 1 {
logFatalf("Error: too many arguments.\n" +
"age accepts a single optional argument for the input file.")
logFatalf("Error: too many arguments: %q.\n"+
"Note that the input file must be specified after all flags.", flag.Args())
}
switch {
case decryptFlag:
if encryptFlag {
logFatalf("Error: -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.")
@@ -140,11 +151,11 @@ func main() {
"Did you mean to use -i/--identity to specify a private key?")
}
default: // encrypt
if len(identityFlags) > 0 {
logFatalf("Error: -i/--identity can't be used in encryption mode.\n" +
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?")
}
if len(recipientFlags) == 0 && len(recipientsFileFlags) == 0 && !passFlag {
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?")
}
@@ -154,6 +165,9 @@ func main() {
if len(recipientsFileFlags) > 0 && passFlag {
logFatalf("Error: -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.")
}
}
var in io.Reader = os.Stdin
@@ -169,11 +183,12 @@ func main() {
stdinInUse = true
}
if name := outFlag; name != "" && name != "-" {
if _, err := os.Stat(name); err == nil {
logFatalf("Error: output file %q exists", name)
}
f := newLazyOpener(name)
defer f.Close()
defer func() {
if err := f.Close(); err != nil {
logFatalf("Error: failed to close output file %q: %v", name, err)
}
}()
out = f
} else if terminal.IsTerminal(int(os.Stdout.Fd())) {
if name != "-" {
@@ -205,7 +220,7 @@ func main() {
}
encryptPass(pass, in, out, armorFlag)
default:
encryptKeys(recipientFlags, recipientsFileFlags, in, out, armorFlag)
encryptKeys(recipientFlags, recipientsFileFlags, identityFlags, in, out, armorFlag)
}
}
@@ -236,7 +251,7 @@ func passphrasePromptForEncryption() (string, error) {
return p, nil
}
func encryptKeys(keys, files []string, in io.Reader, out io.Writer, armor bool) {
func encryptKeys(keys, files, identities []string, in io.Reader, out io.Writer, armor bool) {
var recipients []age.Recipient
for _, arg := range keys {
r, err := parseRecipient(arg)
@@ -252,6 +267,19 @@ func encryptKeys(keys, files []string, in io.Reader, out io.Writer, armor bool)
}
recipients = append(recipients, recs...)
}
for _, name := range identities {
ids, err := parseIdentitiesFile(name)
if err != nil {
logFatalf("Error 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)
}
}
encrypt(recipients, in, out, armor)
}
@@ -264,7 +292,6 @@ func encryptPass(pass string, in io.Reader, out io.Writer, armor bool) {
}
func encrypt(recipients []age.Recipient, in io.Reader, out io.Writer, withArmor bool) {
ageEncrypt := age.Encrypt
if withArmor {
a := armor.NewWriter(out)
defer func() {
@@ -274,7 +301,7 @@ func encrypt(recipients []age.Recipient, in io.Reader, out io.Writer, withArmor
}()
out = a
}
w, err := ageEncrypt(out, recipients...)
w, err := age.Encrypt(out, recipients...)
if err != nil {
logFatalf("Error: %v", err)
}
@@ -293,8 +320,6 @@ func decrypt(keys []string, in io.Reader, out io.Writer) {
&LazyScryptIdentity{passphrasePrompt},
}
// TODO: check the default SSH location if no arguments are provided
// (~/.ssh/id_rsa, ~/.ssh/id_ed25519).
for _, name := range keys {
ids, err := parseIdentitiesFile(name)
if err != nil {
@@ -328,6 +353,20 @@ func passphrasePrompt() (string, error) {
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()
}
return nil, fmt.Errorf("unexpected identity type: %T", id)
}
type lazyOpener struct {
name string
f *os.File
@@ -340,7 +379,7 @@ func newLazyOpener(name string) io.WriteCloser {
func (l *lazyOpener) Write(p []byte) (n int, err error) {
if l.f == nil && l.err == nil {
l.f, l.err = os.OpenFile(l.name, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0666)
l.f, l.err = os.Create(l.name)
}
if l.err != nil {
return 0, l.err
+31 -4
View File
@@ -7,6 +7,7 @@
package main
import (
"errors"
"io/ioutil"
"os"
"path/filepath"
@@ -17,24 +18,40 @@ import (
)
func TestVectors(t *testing.T) {
defaultIDs, err := parseIdentitiesFile("testdata/default_key.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)
}
files, _ := filepath.Glob("testdata/*.age")
for _, f := range files {
_, name := filepath.Split(f)
name = strings.TrimSuffix(name, ".age")
expectFailure := strings.HasPrefix(name, "fail_")
expectNoMatch := strings.HasPrefix(name, "nomatch_")
t.Run(name, func(t *testing.T) {
var identities []age.Identity
identities := defaultIDs
ids, err := parseIdentitiesFile("testdata/" + name + "_key.txt")
if err == nil {
identities = append(identities, ids...)
identities = ids
}
password, err := ioutil.ReadFile("testdata/" + name + "_password.txt")
if err == nil {
i, err := age.NewScryptIdentity(string(password))
p := strings.TrimSpace(string(password))
i, err := age.NewScryptIdentity(p)
if err != nil {
t.Fatal(err)
}
identities = append(identities, i)
identities = []age.Identity{i}
}
in, err := os.Open("testdata/" + name + ".age")
@@ -46,6 +63,16 @@ func TestVectors(t *testing.T) {
if err == nil {
t.Fatal("expected Decrypt failure")
}
if e := (&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) {
t.Errorf("expected ErrIncorrectIdentity, got %v", err)
}
} else {
if err != nil {
t.Fatal(err)
+5 -5
View File
@@ -45,15 +45,15 @@ 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
// readPassphrase reads a passphrase from the terminal. If stdin is not
// connected to a terminal, it tries /dev/tty and fails if that's not available.
// It does not read from a non-terminal stdin, so it does not check stdinInUse.
func readPassphrase() ([]byte, error) {
fd := int(os.Stdin.Fd())
if !terminal.IsTerminal(fd) || stdinInUse {
if !terminal.IsTerminal(fd) {
tty, err := os.Open("/dev/tty")
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, fmt.Errorf("standard input is not a terminal, and opening /dev/tty failed: %v", err)
}
defer tty.Close()
fd = int(tty.Fd())
+10
View File
@@ -22,12 +22,19 @@ import (
"golang.org/x/crypto/ssh"
)
// stdinInUse is set in main. It's a singleton like os.Stdin.
var stdinInUse bool
func parseRecipient(arg string) (age.Recipient, error) {
switch {
case strings.HasPrefix(arg, "age1"):
return age.ParseX25519Recipient(arg)
case strings.HasPrefix(arg, "ssh-"):
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, fmt.Errorf("unknown recipient type: %q", arg)
@@ -109,6 +116,9 @@ func sshKeyType(s string) (string, bool) {
return "", false
}
// parseIdentitiesFile parses a file that contains age or SSH keys. It returns
// one of *age.X25519Identity, *agessh.RSAIdentity, *agessh.Ed25519Identity, or
// *agessh.EncryptedSSHIdentity.
func parseIdentitiesFile(name string) ([]age.Identity, error) {
var f *os.File
if name == "-" {
+6
View File
@@ -0,0 +1,6 @@
# created: 2021-02-02T13:09:43+01:00
# public key: age1xmwwc06ly3ee5rytxm9mflaz2u56jjj36s0mypdrwsvlul66mv4q47ryef
AGE-SECRET-KEY-1EGTZVFFV20835NWYV6270LXYVK2VKNX2MMDKWYKLMGR48UAWX40Q2P2LM0
# TODO: regenerate empty_recipient_body.age
AGE-SECRET-KEY-1TRYTV7PQS5XPUYSTAQZCD7DQCWC7Q77YJD7UVFJRMW4J82Q6930QS70MRX
+1
View File
@@ -0,0 +1 @@
now-major-idea-author-clerk-bronze-all-soul-uncover-glad
+5
View File
@@ -0,0 +1,5 @@
age-encryption.org/v1
-> ssh-ed25519 cp09gQ Kf5JDNFFDUaOvups2MDfP47PlrpJthnmz0WNMfGj+C8
hQ76lMAsG2pjR8GHTU+XU0giePyzE3prVmAw5MbMxSk
--- CjO8qf3vd83otqHSflgWP5gQoe2Roo9tf/zgWEy9t0U
èÙ®Ò%JðÇnj<bÖ: %UgZ4#™±àNÂ韟
+7
View File
@@ -0,0 +1,7 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACB/aTuac9tiWRGrKEtixFlryYlGCPTOpdbmXN9RRmDF2gAAAKDgV/GC4Ffx
ggAAAAtzc2gtZWQyNTUxOQAAACB/aTuac9tiWRGrKEtixFlryYlGCPTOpdbmXN9RRmDF2g
AAAECvFoQXQzXgJLQ+Gz4PfEcfyZwC2gUjOiWTD//mTPyD8H9pO5pz22JZEasoS2LEWWvJ
iUYI9M6l1uZc31FGYMXaAAAAG2ZpbGlwcG9AQmlzdHJvbWF0aC1NMS5sb2NhbAEC
-----END OPENSSH PRIVATE KEY-----
+1
View File
@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH9pO5pz22JZEasoS2LEWWvJiUYI9M6l1uZc31FGYMXa
-1
View File
@@ -1 +0,0 @@
AGE-SECRET-KEY-1TRYTV7PQS5XPUYSTAQZCD7DQCWC7Q77YJD7UVFJRMW4J82Q6930QS70MRX
+6 -5
View File
@@ -1,6 +1,7 @@
age-encryption.org/v1
-> scrypt qeKad+OgIkBbr/ndSa7J3Q 1
C2tmV7/uZjRafxqaQd1JhYkM2KxuHHBy3/d2dJNEZEh8rZCqYfvE/eJUXqiqZsZa
6kWgG1qa6Q6sXPz0vIIpYHGf4gzxG9oTVonMke2kHC4
--- FQeacPQobvFBd0tuIQnQDd/NEDR4G4MfylkXiq9ZqZ0
Ăů€pŰÂt€tŤ�Ë3qňĆ)¬ĚČŚv´¨QĆoĚšK÷¶7ÜŘ)ř%a
-> scrypt z8U9dYMQuK1fFdvtpQYLEQ 10
5SVjw1bbFCZLdI1FR7RqfTd3yWo4KS1ikOjvz60Bpqhrv0W6o6/2oszxZEm1gEUC
--- YXxSwONGPBbV7woMuEFTYuA03qTYUF1k0Y8j/NDEu1o
’Ě
Ĺ!í‡ iŹŢ.f¦ćš{źdËIE]© ńn”$Č!b®2
@@ -1 +0,0 @@
dog-old-little-breeze-novel-razor-battle-replace-lake-horse
+5 -5
View File
@@ -1,6 +1,6 @@
age-encryption.org/v1
-> X25519 /Gt0E6JT7yuYHlwsGW5LbpEEJawOc+QMeMAS+hoOIgw
XU/4Zkz4MksDhge0kosiMTJF8tHnOP0ZSi+6aaMqLMS1PlMIs95nKz3H7JGesTwA
tsxuQrj+TuoGouNB1O0VshA9vsHGurn0Dtw5e7bkw9Q
--- jQNSF6blozj2QFYJ/2iqy0wUcPuz/8vCS7RgKH8wjNI
Þ9­¶‡yË_£§R\¯ÿm\¥û©Uv6Qȶ»ñmK»a¢©êv®©˜2
-> X25519 UkSgrxSETNpdkHY8EwiiRivqks2QJLUzsNsVjUTDcmw
8yB9TqsBo4Ypchw07AtemV5TW4sGwyPDPMIfRg8Ve8rbDXt4tCwnnKcMq2K6aoqx
--- vUhLU0U9Dc8YhbKy4SxKuq0iSqqjBWGnHfZG+9+O4v4
タケ��イ€hエW諾餞I暠fッニ�索Q;ミRh皷w
-3
View File
@@ -1,3 +0,0 @@
# created: 2020-09-19T18:42:11+02:00
# public key: age1uc8zlurjyjpenrslc2thyl28u7ylz6x8c2g9yphvjha6xm8ppf3slq0l25
AGE-SECRET-KEY-1D8JAD8SXNFVQEFHAUNNAX4QCE3K5CUKMT7YYHNGTUSSP97YGWL4STV89UH
Binary file not shown.
+5
View File
@@ -0,0 +1,5 @@
age-encryption.org/v1
-> scrypt 1Q6WlGmsRulbN7bmUw8A1Q 23
GP2lnzFuk1dgEkcMPmK6KkmuOm5gIWJzLeuwGcRsvAY
--- vXvOsVbDbMc0x5Js1FS6k1ViOJ3H2ZdSUZo9bfvbzmU
=ñÅÙœ£¨v>¹vhKMû'ÕßúÙ½NeçS†Äñ\(_ó
+5
View File
@@ -0,0 +1,5 @@
age-encryption.org/v1
-> scrypt X6oOTRAjCR1xid0PlnNMFA 10
hszKAHhyFVpUgt9niYpdYXVhhN+r+oiCLPZukDdQZBQ
--- 7BRJPVjbIC1JntvHrA13PQrnsa3lkwhnNF/Pbo4BPs4
4|)ÆS|Û‹Ò¿D�÷§}’Í2è%�e�=óœ6˜»ÇZ
+5
View File
@@ -0,0 +1,5 @@
age-encryption.org/v1
-> X25519 Rp86RQ3LgUJpQy4X2RMUhURlBP28tCaLQ2ssysJfRhg
83YXad/lj3/wFM4n7vlGIiBSgfhG8lfiP5U7ajjK3HM
--- O2+UpzetsP2+7BPyGQ4C6VMTY6zwp5TiNpVcFy4qdyM
 璇漡�(擔Q:|c蔑鞮蓤�=f�6b坿!
+13
View File
@@ -0,0 +1,13 @@
age-encryption.org/v1
-> ssh-rsa jw/33g
xv12FD3f2d7snIcuXBznTOWAlgCovW1Dqttk9uljWKy3GtRZ3t8jEWkQEOYkOk0M
EHA6sHWfdPLdlS3DjQYjcaLFvwh3+XVKYNzP9MhLg8P8xvxVkn4aiCKd8ivEisp0
bKGi4g/TJz/JKUg1SGbqDg966to0P5AWrkwAD7OMykQToqo56flrKXgFPleSWVWu
umiwbxFYs7ltbRYvjzdpIj9l30lXkzrADP3RrrvTu/qT0IN3PMi3bOqm0kKz0vkd
p4NpxKmfqQXavU+YZiyQL637V3cbKIAEJ1qmpkd2Tr2oUhfD5IgAoT1nC5tCIzRb
DkPwM4k2FJgVX0KKvW3i0+k5tve4XWg82vq2OCj8+sl3A8cLX3g5zhh53DovUBVm
qDU2HWf++3q9kUy1al0sFb2es4ih+tK74nPjBJZtX0n+4lMngz557+XuYnzZ2OkW
QEq3b7Trdidw7Ak9S14tdXhj8oy7J1jdHsQ8/wehAc1v8MuBb1O7LxVIFxzBEBCA
--- QdCY4BN4vwp5jb+AFsyoHkvKW+EneZsZjPURH2tCF18
«)KVMARsS‘M’ÚëûØ‚Kí>›ÕÕ«`¤ ~T0n·
+38
View File
@@ -0,0 +1,38 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
NhAAAAAwEAAQAAAYEA1C04rdClHoW4oG4bEGmaNqFy4DLoPJ0358w4XH+XBM3TiWcheouW
kUG6m1yDmHk0t0oaaf4hOnetKovdyQQX73gGaq++rSu5VSvH7LbwABoG6PS/UbuZ4Vl9B0
5WVDqHVE9hNK4AHqBc373GU2mo8z5opKxEprmiS3HSd3K2wiMqL5E8XPOSm0p/isuYK57X
VUexl73tB7iIMLklxjcjtP4REMoQhHKOMOdy2Q15dw5cYG+drtEArBRYkCZmd0Vp2ws9pj
YzPVaOSkbdqSeLu+JVbH1wrwKhuBrA3eVlwjUTWkO4FHcNXkp773Mt4cXhKizTfbR2hQox
Lsj31301Xd7dEpV63sqDW1e+a2L2dhemi8cjDMrPuW6Z19Lbti0quAb4+cSLAaJI4BHd1F
8o9XhK7EHVCdIIIQDKVzo1WyEsDwBjL1LB9rpxm4732sZyue0uygFzmM544QX+WsiJXgHP
uC1Q/ynjLRm6ZMl16MwvY8B/XGQWxlOAbRJQG84fAAAFmEwAjV1MAI1dAAAAB3NzaC1yc2
EAAAGBANQtOK3QpR6FuKBuGxBpmjahcuAy6DydN+fMOFx/lwTN04lnIXqLlpFBuptcg5h5
NLdKGmn+ITp3rSqL3ckEF+94Bmqvvq0ruVUrx+y28AAaBuj0v1G7meFZfQdOVlQ6h1RPYT
SuAB6gXN+9xlNpqPM+aKSsRKa5oktx0ndytsIjKi+RPFzzkptKf4rLmCue11VHsZe97Qe4
iDC5JcY3I7T+ERDKEIRyjjDnctkNeXcOXGBvna7RAKwUWJAmZndFadsLPaY2Mz1WjkpG3a
kni7viVWx9cK8CobgawN3lZcI1E1pDuBR3DV5Ke+9zLeHF4Sos0320doUKMS7I99d9NV3e
3RKVet7Kg1tXvmti9nYXpovHIwzKz7lumdfS27YtKrgG+PnEiwGiSOAR3dRfKPV4SuxB1Q
nSCCEAylc6NVshLA8AYy9Swfa6cZuO99rGcrntLsoBc5jOeOEF/lrIiV4Bz7gtUP8p4y0Z
umTJdejML2PAf1xkFsZTgG0SUBvOHwAAAAMBAAEAAAGBAKytAOu0Wi009sTZ1vzMdMzxJ+
R+ibKK4Oysr1HYJLesKvQwEncBE1C0BYJbEF4OhnCExmpsf+5tZ2iw25a01iX1sIMy9CNK
6lH+h36Gg1wR0n3Ucb+6xck4YyCHCIsT9v8OezW8Riympe8RK07HNtB/gfpCmLx3ZzWvNH
Ix0bq9k5+Su2WKdU4cmyACAZ2+b9DfwBCWaUlXTL8abzuZtF2gR5M6X6bq8/2o3zb2WFwk
O9nf/JxBTCK/jDQEjG+U9MyGxZIW5DeG1nNFtOzJoT8krIkeSOjQ5XQrkjCw+yihSCWMG+
s+SKO77u30SO7OCENsFIXpUzpt6+JmazlXjLW/OdYNooQMHtqCZzVMRgxiy3gDGF35YvgV
VnP5gVEW9HEZ0kD+x4Rl2kB6bV7jMi8BXrazQ1EmTasJFg1pv6iRJWzY1JoP2kRfgiHGL6
OqgrXakqo3hMJuz+JRU2/hlF13743MiIxpcbaaRqURoWuNRLHitVWE35/XVCez0C6OwQAA
AMEAoh106+3JbiZI19iRICR247IoOLpSSed98eQj+l3OYfJ86cQipSjxdSPWcP58yyyElY
d9q6K16sDTLAlRJzF7MFxSc80JY6RgFq/Sy4Jm0/Z10wwJhTgOkxq6IynzLnO7goRirE31
jxGif4nI2IYEQvv6MOD8TWA4axxGMw2StYB6P4R5peozf81oR6m79ERIDSkrm0RYYn931r
gVuxvo3ABVxMtg1lV80LJMayy87Oi8BehGBxMBgsKtQaH8+5h7AAAAwQD+8lJpBcrrHQKk
3o2XAZxB5Fool4f2iuZWTxA1vq0/TCUcEodrdWfLuDeVbDsFemW0vBSkKzf4NlZSs2DAKl
YWT6y18eyDyJXn0TNVTeO3F5mkkX5spqbjDcESSs3whIuDqXU++3sII7iMzGw50tDP4Dw6
TViEVM3anpeqlAbkciR5o9IJx3nRcGh81Bs4gticcRF0vqiJoAhNlSZXR1XMjevwt68i+4
RKPPQsTM7uJLm236VUhDivO1OJcBTLW7MAAADBANUNqH+//G4gIruBO3BsIvbzDw0DgRam
R1tqqn4g53boiv1RPtUJ2GbkCsisy5pU+JdTN7ekFEF8KWuunjImkfVyAiTFsHHmOoXV3Z
EX0mNDXOlKOP2YAIMrDt5CkPdEh6qQG21LCZXTWmwheZ9iN2vOl/fKqUW9lqd/kTe6WsON
hIpZhs2+oz54Riq1ZwzO9NkcYrvZoDKbDopL1r2ibw0mkgCJrxpWi0Yt2Iooh4GXXqP5C9
T8hrZCbrVJkjKd5QAAABtmaWxpcHBvQEJpc3Ryb21hdGgtTTEubG9jYWwBAgMEBQY=
-----END OPENSSH PRIVATE KEY-----
+1
View File
@@ -0,0 +1 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDULTit0KUehbigbhsQaZo2oXLgMug8nTfnzDhcf5cEzdOJZyF6i5aRQbqbXIOYeTS3Shpp/iE6d60qi93JBBfveAZqr76tK7lVK8fstvAAGgbo9L9Ru5nhWX0HTlZUOodUT2E0rgAeoFzfvcZTaajzPmikrESmuaJLcdJ3crbCIyovkTxc85KbSn+Ky5grntdVR7GXve0HuIgwuSXGNyO0/hEQyhCEco4w53LZDXl3Dlxgb52u0QCsFFiQJmZ3RWnbCz2mNjM9Vo5KRt2pJ4u74lVsfXCvAqG4GsDd5WXCNRNaQ7gUdw1eSnvvcy3hxeEqLNN9tHaFCjEuyPfXfTVd3t0SlXreyoNbV75rYvZ2F6aLxyMMys+5bpnX0tu2LSq4Bvj5xIsBokjgEd3UXyj1eErsQdUJ0gghAMpXOjVbISwPAGMvUsH2unGbjvfaxnK57S7KAXOYznjhBf5ayIleAc+4LVD/KeMtGbpkyXXozC9jwH9cZBbGU4BtElAbzh8=
+5
View File
@@ -0,0 +1,5 @@
age-encryption.org/v1
-> scrypt qEa/WztCd2KJ4mKwNf1Yrw 10
TQZ4GpAaH4aR4oSDWZTgeRT4wRby4jwmtB02dElWmVQ
--- kOiEP6uoMyK9GKIsV77o4oaPuEr2Q0vdcu+1RKC3lLU
hŸo²P¤V w§\5~4ôënE½oÕd>rOš°mÒÛ¨
+88
View File
@@ -0,0 +1,88 @@
.\" generated with Ronn/v0.7.3
.\" http://github.com/rtomayko/ronn/tree/0.7.3
.
.TH "AGE\-KEYGEN" "1" "October 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
View 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'>October 2021</li>
<li class='tr'>age-keygen(1)</li>
</ol>
</div>
</body>
</html>
+57
View 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>
+296
View File
@@ -0,0 +1,296 @@
.\" generated with Ronn/v0.7.3
.\" http://github.com/rtomayko/ronn/tree/0.7.3
.
.TH "AGE" "1" "October 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 option 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\. 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
c\. "\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 successful for the full length of the input\.
.
.P
If an error occurs during decryption, partial output might still be generated, but only if it was possible to securely authenticate it\. No unauthenticathed output is ever released\.
.
.SH "BACKWARDS COMPATIBILITY"
Files encrypted with a stable version (not alpha, beta, or release candidate) of \fBage\fR, or with any v1\.0\.0 beta or release candidate, will decrypt with any later version of the tool\.
.
.P
If decrypting older files poses a security risk, doing so might cause an error by default\. In this case, a flag will be provided to force the operation\.
.
.SH "EXAMPLES"
Generate a new identity, encrypt data, and decrypt:
.
.IP "" 4
.
.nf
$ age\-keygen \-o key\.txt
Public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
$ tar cvz ~/data | age \-r age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p > data\.tar\.gz\.age
$ age \-d \-o data\.tar\.gz \-i key\.txt data\.tar\.gz\.age
.
.fi
.
.IP "" 0
.
.P
Encrypt \fBexample\.jpg\fR to multiple recipients and output to \fBexample\.jpg\.age\fR:
.
.IP "" 4
.
.nf
$ age \-o example\.jpg\.age \-r age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p \e
\-r age1lggyhqrw2nlhcxprm67z43rta597azn8gknawjehu9d9dl0jq3yqqvfafg example\.jpg
.
.fi
.
.IP "" 0
.
.P
Encrypt to a list of recipients:
.
.IP "" 4
.
.nf
$ cat > recipients\.txt
# Alice
age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
# 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 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
+328
View File
@@ -0,0 +1,328 @@
<!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 option 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. 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> c. "<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 successful 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. In this case, 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 &gt; 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 &gt; recipients.txt
# Alice
age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
# Bob
age1lggyhqrw2nlhcxprm67z43rta597azn8gknawjehu9d9dl0jq3yqqvfafg
$ age -R recipients.txt example.jpg &gt; example.jpg.age
</code></pre>
<p>Encrypt and decrypt a file using a passphrase:</p>
<pre><code>$ age -p secrets.txt &gt; 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 &gt; secrets.txt
Enter passphrase:
</code></pre>
<p>Encrypt and decrypt with an SSH public key:</p>
<pre><code>$ age -R ~/.ssh/id_ed25519.pub example.jpg &gt; example.jpg.age
$ age -d -i ~/.ssh/id_ed25519 example.jpg.age &gt; 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 &gt; 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'>October 2021</li>
<li class='tr'>age(1)</li>
</ol>
</div>
</body>
</html>
+239
View File
@@ -0,0 +1,239 @@
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 option 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\. 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.
c\. "`-`", 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 successful 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. In this case, 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 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>
+1 -5
View File
@@ -2,8 +2,4 @@ module filippo.io/age
go 1.13
require (
filippo.io/edwards25519 v1.0.0-alpha.2
github.com/sergi/go-diff v1.1.0
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59
)
require golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad
+6 -27
View File
@@ -1,31 +1,10 @@
filippo.io/edwards25519 v1.0.0-alpha.2 h1:EWbZLqGEPSIj2W69gx04KtNVkyPIfe3uj0DhDQJonbQ=
filippo.io/edwards25519 v1.0.0-alpha.2/go.mod h1:X+pm78QAUPtFLi1z9PYIlS/bdDnvbCOGKtZ+ACWEf7o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59 h1:3zb4D3T4G8jdExgVU/95+vQXfpEPiMdCaZgmGVxjNHM=
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
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-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-21
View File
@@ -1,21 +0,0 @@
# Copyright 2019 Google LLC
#
# Use of this source code is governed by a BSD-style
# license that can be found in the LICENSE file or at
# https://developers.google.com/open-source/licenses/bsd
# docker run --rm -v $PWD/workdir:/workdir $(docker build -q -f internal/format/Dockerfile.go-fuzz .)
FROM golang:1.14-alpine3.11
RUN apk add --no-cache git
RUN go get github.com/dvyukov/go-fuzz/...
ADD . $GOPATH/src/filippo.io/age/
WORKDIR $GOPATH/src/filippo.io/age
RUN go-fuzz-build ./internal/format
VOLUME /workdir
ENTRYPOINT ["go-fuzz", "-workdir", "/workdir", "-bin", "format-fuzz.zip"]
+34 -10
View File
@@ -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
}
-60
View File
@@ -1,60 +0,0 @@
// Copyright 2019 Google LLC
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
// +build gofuzz
package format
import (
"bytes"
"fmt"
"io"
"strings"
"github.com/sergi/go-diff/diffmatchpatch"
)
func Fuzz(data []byte) int {
isArmored := bytes.HasPrefix(data, []byte("-----BEGIN AGE ENCRYPTED FILE-----"))
h, payload, err := Parse(bytes.NewReader(data))
if err != nil {
if h != nil {
panic("h != nil on error")
}
if payload != nil {
panic("payload != nil on error")
}
return 0
}
w := &bytes.Buffer{}
if isArmored {
w := ArmoredWriter(w)
if err := h.Marshal(w); err != nil {
panic(err)
}
if _, err := io.Copy(w, payload); err != nil {
if strings.Contains(err.Error(), "invalid armor") {
return 0
}
panic(err)
}
w.Close()
} else {
if err := h.Marshal(w); err != nil {
panic(err)
}
if _, err := io.Copy(w, payload); err != nil {
panic(err)
}
}
if !bytes.Equal(w.Bytes(), data) {
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(string(data), string(w.Bytes()), false)
fmt.Println(dmp.DiffToDelta(diffs))
panic("Marshal output different from input")
}
return 1
}
+4 -2
View File
@@ -9,7 +9,7 @@ package age
import (
"crypto/hmac"
"crypto/sha256"
"fmt"
"errors"
"io"
"filippo.io/age/internal/format"
@@ -32,6 +32,8 @@ func aeadEncrypt(key, plaintext []byte) ([]byte, error) {
return aead.Seal(nil, nonce, plaintext, nil), nil
}
var errIncorrectCiphertextSize = errors.New("encrypted value has unexpected length")
// aeadDecrypt decrypts a message of an expected fixed size.
//
// The message size is limited to mitigate multi-key attacks, where a ciphertext
@@ -43,7 +45,7 @@ func aeadDecrypt(key []byte, size int, ciphertext []byte) ([]byte, error) {
return nil, err
}
if len(ciphertext) != size+aead.Overhead() {
return nil, fmt.Errorf("encrypted message has unexpected length")
return nil, errIncorrectCiphertextSize
}
nonce := make([]byte, chacha20poly1305.NonceSize)
return aead.Open(nil, nonce, ciphertext, nil)
+3 -1
View File
@@ -164,7 +164,9 @@ func (i *ScryptIdentity) unwrap(block *Stanza) ([]byte, error) {
// only one bit. This also does not bypass any scrypt work, although that work
// can be precomputed in an online oracle scenario.
fileKey, err := aeadDecrypt(k, fileKeySize, block.Body)
if err != nil {
if err == errIncorrectCiphertextSize {
return nil, errors.New("invalid scrypt recipient block: incorrect file key size")
} else if err != nil {
return nil, ErrIncorrectIdentity
}
return fileKey, nil
+1 -1
View File
@@ -1,2 +1,2 @@
# Test key for ExampleParseX25519Identities.
# Test key for ExampleParseIdentities.
AGE-SECRET-KEY-184JMZMVQH3E6U0PSL869004Y3U2NYV7R30EU99CSEDNPH02YUVFSZW44VU
+3 -1
View File
@@ -188,7 +188,9 @@ func (i *X25519Identity) unwrap(block *Stanza) ([]byte, error) {
}
fileKey, err := aeadDecrypt(wrappingKey, fileKeySize, block.Body)
if err != nil {
if err == errIncorrectCiphertextSize {
return nil, errors.New("invalid X25519 recipient block: incorrect file key size")
} else if err != nil {
return nil, ErrIncorrectIdentity
}
return fileKey, nil