8 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
35 changed files with 282 additions and 567 deletions

View File

@@ -1,6 +1,6 @@
---
name: Bug report 🐞
about: Did you encounter a bug in this implementation?
name: Bug report
about: Create a report about a bug in this implementation.
title: ''
labels: ''
assignees: ''

View File

@@ -1,10 +0,0 @@
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 Normal file
View File

@@ -0,0 +1,15 @@
---
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 Normal file
View File

@@ -0,0 +1,21 @@
---
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>
```

View File

@@ -39,9 +39,7 @@ 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:

View File

@@ -15,12 +15,14 @@ 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:

View File

@@ -6,7 +6,7 @@ jobs:
strategy:
fail-fast: false
matrix:
go: [1.16.x, 1.17.x]
go: [1.15.x, 1.16.x]
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:

View File

@@ -7,9 +7,8 @@
class Age < Formula
desc "Simple, modern, secure file encryption"
homepage "https://filippo.io/age"
url "https://github.com/FiloSottile/age/archive/v1.0.0-rc.3.zip"
sha256 "0e7d94f17e610d5ad9ce8e88e3c157b073dcc41984b1d07793aef44b9e3b67d8"
head "https://github.com/FiloSottile/age.git"
url "https://github.com/FiloSottile/age/archive/v1.0.0-beta6.zip"
sha256 "6ffa23aee0f03c3e00707915e4300591847a2b0c5157ca7a696eb39bfeb7359c"
depends_on "go" => :build

144
README.md
View File

@@ -1,9 +1,8 @@
<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>
# age
[![Go Reference](https://pkg.go.dev/badge/filippo.io/age.svg)](https://pkg.go.dev/filippo.io/age)
[![man page](https://img.shields.io/badge/man-page-lightgrey)](https://htmlpreview.github.io/?https://github.com/FiloSottile/age/blob/master/doc/age.1.html)
[![pkg.go.dev](https://pkg.go.dev/badge/filippo.io/age)](https://pkg.go.dev/filippo.io/age)
age is a simple, modern and secure file encryption tool, format, and Go library.
age is a simple, modern and secure file encryption tool, format, and library.
It features small explicit keys, no config options, and UNIX-style composability.
@@ -14,34 +13,28 @@ $ tar cvz ~/data | age -r age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9
$ age --decrypt -i key.txt data.tar.gz.age > data.tar.gz
```
The format specification is at [age-encryption.org/v1](https://age-encryption.org/v1). age was designed by [@Benjojo12](https://twitter.com/Benjojo12) and [@FiloSottile](https://twitter.com/FiloSottile).
The format specification is at [age-encryption.org/v1](https://age-encryption.org/v1). 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).
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 [--encrypt] (-r RECIPIENT | -R PATH)... [--armor] [-o OUTPUT] [INPUT]
age [--encrypt] --passphrase [--armor] [-o OUTPUT] [INPUT]
age (-r RECIPIENT | -R PATH)... [--armor] [-o OUTPUT] [INPUT]
age --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...").
@@ -52,12 +45,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. Passphrase encrypted age files can be used as
identity files. Multiple key files can be provided, and any unused ones
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.
```
### Multiple recipients
@@ -96,22 +85,6 @@ $ 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.)
@@ -135,89 +108,40 @@ Keep in mind that people might not protect SSH keys long-term, since they are re
## Installation
<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.
On macOS or Linux, you can use Homebrew:
```
https://dl.filippo.io/age/latest?for=linux/amd64
https://dl.filippo.io/age/v1.0.0-rc.1?for=darwin/arm64
...
brew tap filippo.io/age https://filippo.io/age
brew install age
```
If your system has [Go 1.13+](https://golang.org/dl/), you can build from source.
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:
```
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.

21
age.go
View File

@@ -113,16 +113,6 @@ 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
@@ -138,6 +128,11 @@ 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 {
@@ -184,6 +179,12 @@ func Decrypt(src io.Reader, identities ...Identity) (io.Reader, error) {
return nil, fmt.Errorf("failed to read header: %v", err)
}
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 {
stanzas = append(stanzas, (*Stanza)(s))

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"
@@ -189,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 := new(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"

View File

@@ -24,7 +24,6 @@ import (
// pass the result to NewEd25519Identity or NewRSAIdentity.
type EncryptedSSHIdentity struct {
pubKey ssh.PublicKey
recipient age.Recipient
pemBytes []byte
passphrase func() ([]byte, error)
@@ -42,34 +41,22 @@ 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) {
i := &EncryptedSSHIdentity{
pubKey: pubKey,
pemBytes: pemBytes,
passphrase: passphrase,
}
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
case "ssh-ed25519", "ssh-rsa":
default:
return nil, fmt.Errorf("unsupported SSH key type: %v", t)
}
return i, nil
return &EncryptedSSHIdentity{
pubKey: pubKey,
pemBytes: pemBytes,
passphrase: passphrase,
}, nil
}
var _ age.Identity = &EncryptedSSHIdentity{}
func (i *EncryptedSSHIdentity) Recipient() age.Recipient {
return i.recipient
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

View File

@@ -16,7 +16,7 @@ import (
"time"
"filippo.io/age"
"golang.org/x/term"
"golang.org/x/crypto/ssh/terminal"
)
const usage = `Usage:
@@ -70,10 +70,10 @@ func main() {
flag.StringVar(&outFlag, "output", "", "output to `FILE` (default stdout)")
flag.Parse()
if len(flag.Args()) != 0 && !convertFlag {
errorf("too many arguments")
log.Fatalf("age-keygen takes no arguments")
}
if len(flag.Args()) > 1 && convertFlag {
errorf("too many arguments")
log.Fatalf("Too many arguments")
}
if versionFlag {
if Version != "" {
@@ -92,11 +92,11 @@ func main() {
if outFlag != "" {
f, err := os.OpenFile(outFlag, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
if err != nil {
errorf("failed to open output file %q: %v", outFlag, err)
log.Fatalf("Failed to open output file %q: %v", outFlag, err)
}
defer func() {
if err := f.Close(); err != nil {
errorf("failed to close output file %q: %v", outFlag, err)
log.Fatalf("Failed to close output file %q: %v", outFlag, err)
}
}()
out = f
@@ -106,7 +106,7 @@ func main() {
if inFile := flag.Arg(0); inFile != "" && inFile != "-" {
f, err := os.Open(inFile)
if err != nil {
errorf("failed to open input file %q: %v", inFile, err)
log.Fatalf("Failed to open input file %q: %v", inFile, err)
}
defer f.Close()
in = f
@@ -116,7 +116,7 @@ func main() {
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")
fmt.Fprintf(os.Stderr, "Warning: writing secret key to a world-readable file.\n")
}
generate(out)
}
@@ -125,10 +125,10 @@ func main() {
func generate(out *os.File) {
k, err := age.GenerateX25519Identity()
if err != nil {
errorf("internal error: %v", err)
log.Fatalf("Internal error: %v", err)
}
if !term.IsTerminal(int(out.Fd())) {
if !terminal.IsTerminal(int(out.Fd())) {
fmt.Fprintf(os.Stderr, "Public key: %s\n", k.Recipient())
}
@@ -140,24 +140,16 @@ func generate(out *os.File) {
func convert(in io.Reader, out io.Writer) {
ids, err := age.ParseIdentities(in)
if err != nil {
errorf("failed to parse input: %v", err)
log.Fatalf("Failed to parse input: %v", err)
}
if len(ids) == 0 {
errorf("no identities found in the input")
log.Fatalf("No identities found in the input")
}
for _, id := range ids {
id, ok := id.(*age.X25519Identity)
if !ok {
errorf("internal error: unexpected identity type: %T", id)
log.Fatalf("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)
}

View File

@@ -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/term"
"golang.org/x/crypto/ssh/terminal"
)
type multiFlag []string
@@ -59,8 +59,7 @@ 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. Passphrase encrypted age files can be used as
identity files. Multiple key files can be provided, and any unused ones
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
@@ -78,7 +77,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 {
@@ -127,47 +126,47 @@ func main() {
}
if flag.NArg() > 1 {
errorWithHint(fmt.Sprintf("too many arguments: %q", flag.Args()),
"note that the input file must be specified after all flags")
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 {
errorf("-e/--encrypt can't be used with -d/--decrypt")
logFatalf("Error: -e/--encrypt can't be used with -d/--decrypt.")
}
if armorFlag {
errorWithHint("-a/--armor can't be used with -d/--decrypt",
"note that armored files are detected automatically")
logFatalf("Error: -a/--armor can't be used with -d/--decrypt.\n" +
"Note that armored files are detected automatically.")
}
if passFlag {
errorWithHint("-p/--passphrase can't be used with -d/--decrypt",
"note that password protected files are detected automatically")
logFatalf("Error: -p/--passphrase can't be used with -d/--decrypt.\n" +
"Note that password protected files are detected automatically.")
}
if len(recipientFlags) > 0 {
errorWithHint("-r/--recipient can't be used with -d/--decrypt",
"did you mean to use -i/--identity to specify a private key?")
logFatalf("Error: -r/--recipient can't be used with -d/--decrypt.\n" +
"Did you mean to use -i/--identity to specify a private key?")
}
if len(recipientsFileFlags) > 0 {
errorWithHint("-R/--recipients-file can't be used with -d/--decrypt",
"did you mean to use -i/--identity to specify a private key?")
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?")
}
default: // encrypt
if len(identityFlags) > 0 && !encryptFlag {
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?")
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)+len(recipientsFileFlags)+len(identityFlags) == 0 && !passFlag {
errorWithHint("missing recipients",
"did you forget to specify -r/--recipient, -R/--recipients-file or -p/--passphrase?")
logFatalf("Error: missing recipients.\n" +
"Did you forget to specify -r/--recipient, -R/--recipients-file or -p/--passphrase?")
}
if len(recipientFlags) > 0 && passFlag {
errorf("-p/--passphrase can't be combined with -r/--recipient")
logFatalf("Error: -p/--passphrase can't be combined with -r/--recipient.")
}
if len(recipientsFileFlags) > 0 && passFlag {
errorf("-p/--passphrase can't be combined with -R/--recipients-file")
logFatalf("Error: -p/--passphrase can't be combined with -R/--recipients-file.")
}
if len(identityFlags) > 0 && passFlag {
errorf("-p/--passphrase can't be combined with -i/--identity")
logFatalf("Error: -p/--passphrase can't be combined with -i/--identity.")
}
}
@@ -176,7 +175,7 @@ func main() {
if name := flag.Arg(0); name != "" && name != "-" {
f, err := os.Open(name)
if err != nil {
errorf("failed to open input file %q: %v", name, err)
logFatalf("Error: failed to open input file %q: %v", name, err)
}
defer f.Close()
in = f
@@ -187,23 +186,22 @@ func main() {
f := newLazyOpener(name)
defer func() {
if err := f.Close(); err != nil {
errorf("failed to close output file %q: %v", name, err)
logFatalf("Error: failed to close output file %q: %v", name, err)
}
}()
out = f
} else if term.IsTerminal(int(os.Stdout.Fd())) {
} else if terminal.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 -".
errorWithHint("refusing to output binary to the terminal",
"did you mean to use -a/--armor?",
`force anyway with "-o -"`)
logFatalf("Error: refusing to output binary to the terminal.\n" +
`Did you mean to use -a/--armor? Force with "-o -".`)
}
}
if in == os.Stdin && term.IsTerminal(int(os.Stdin.Fd())) {
if in == os.Stdin && terminal.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{}
@@ -218,7 +216,7 @@ func main() {
case passFlag:
pass, err := passphrasePromptForEncryption()
if err != nil {
errorf("%v", err)
logFatalf("Error: %v", err)
}
encryptPass(pass, in, out, armorFlag)
default:
@@ -227,7 +225,8 @@ func main() {
}
func passphrasePromptForEncryption() (string, error) {
pass, err := readPassphrase("Enter passphrase (leave empty to autogenerate a secure one):")
fmt.Fprintf(os.Stderr, "Enter passphrase (leave empty to autogenerate a secure one): ")
pass, err := readPassphrase()
if err != nil {
return "", fmt.Errorf("could not read passphrase: %v", err)
}
@@ -238,10 +237,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 {
confirm, err := readPassphrase("Confirm passphrase:")
fmt.Fprintf(os.Stderr, "Confirm passphrase: ")
confirm, err := readPassphrase()
if err != nil {
return "", fmt.Errorf("could not read passphrase: %v", err)
}
@@ -256,33 +255,30 @@ 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 {
errorf("%v", err)
logFatalf("Error: %v", err)
}
recipients = append(recipients, r)
}
for _, name := range files {
recs, err := parseRecipientsFile(name)
if err != nil {
errorf("failed to parse recipient file %q: %v", name, err)
logFatalf("Error: failed to parse recipient file %q: %v", name, err)
}
recipients = append(recipients, recs...)
}
for _, name := range identities {
ids, err := parseIdentitiesFile(name)
if err != nil {
errorf("reading %q: %v", name, err)
logFatalf("Error reading %q: %v", name, err)
}
r, err := identitiesToRecipients(ids)
if err != nil {
errorf("internal error processing %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)
}
recipients = append(recipients, r...)
}
encrypt(recipients, in, out, armor)
}
@@ -290,7 +286,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 {
errorf("%v", err)
logFatalf("Error: %v", err)
}
encrypt([]age.Recipient{r}, in, out, armor)
}
@@ -300,20 +296,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 {
errorf("%v", err)
logFatalf("Error: %v", err)
}
}()
out = a
}
w, err := age.Encrypt(out, recipients...)
if err != nil {
errorf("%v", err)
logFatalf("Error: %v", err)
}
if _, err := io.Copy(w, in); err != nil {
errorf("%v", err)
logFatalf("Error: %v", err)
}
if err := w.Close(); err != nil {
errorf("%v", err)
logFatalf("Error: %v", err)
}
}
@@ -327,7 +323,7 @@ func decrypt(keys []string, in io.Reader, out io.Writer) {
for _, name := range keys {
ids, err := parseIdentitiesFile(name)
if err != nil {
errorf("reading %q: %v", name, err)
logFatalf("Error reading %q: %v", name, err)
}
identities = append(identities, ids...)
}
@@ -341,44 +337,34 @@ func decrypt(keys []string, in io.Reader, out io.Writer) {
r, err := age.Decrypt(in, identities...)
if err != nil {
errorf("%v", err)
logFatalf("Error: %v", err)
}
if _, err := io.Copy(out, r); err != nil {
errorf("%v", err)
logFatalf("Error: %v", err)
}
}
func passphrasePrompt() (string, error) {
pass, err := readPassphrase("Enter passphrase:")
fmt.Fprintf(os.Stderr, "Enter passphrase: ")
pass, err := readPassphrase()
if err != nil {
return "", fmt.Errorf("could not read passphrase: %v", err)
}
return string(pass), nil
}
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)
}
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 recipients, nil
return nil, fmt.Errorf("unexpected identity type: %T", id)
}
type lazyOpener struct {
@@ -408,19 +394,8 @@ func (l *lazyOpener) Close() error {
return nil
}
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")
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 ]")
}

View File

@@ -18,30 +18,24 @@ import (
)
func TestVectors(t *testing.T) {
var defaultIDs []age.Identity
defaultIDs, err := parseIdentitiesFile("testdata/default_key.txt")
if err != nil {
t.Fatal(err)
}
password, err := ioutil.ReadFile("testdata/default_password.txt")
if err != nil {
t.Fatal(err)
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) {
@@ -69,17 +63,17 @@ func TestVectors(t *testing.T) {
if err == nil {
t.Fatal("expected Decrypt failure")
}
if e := new(age.NoIdentityMatchError); errors.As(err, &e) {
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 := new(age.NoIdentityMatchError); !errors.As(err, &e) {
if e := (&age.NoIdentityMatchError{}); !errors.As(err, &e) {
t.Errorf("expected ErrIncorrectIdentity, got %v", err)
}
} else if expectPass {
} else {
if err != nil {
t.Fatal(err)
}
@@ -88,8 +82,6 @@ func TestVectors(t *testing.T) {
t.Fatal(err)
}
t.Logf("%s", out)
} else {
t.Fatal("invalid test vector")
}
})
}

View File

@@ -7,14 +7,12 @@
package main
import (
"bytes"
"errors"
"fmt"
"os"
"runtime"
"filippo.io/age"
"golang.org/x/term"
"golang.org/x/crypto/ssh/terminal"
)
type LazyScryptIdentity struct {
@@ -24,11 +22,6 @@ 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
}
@@ -52,93 +45,23 @@ func (i *LazyScryptIdentity) Unwrap(stanzas []*age.Stanza) (fileKey []byte, err
return fileKey, err
}
type EncryptedIdentity struct {
Contents []byte
Passphrase func() (string, error)
NoMatchWarning func()
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
}
// 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) {
tty, err := os.Open("/dev/tty")
if err != nil {
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
return nil, fmt.Errorf("standard input is not a terminal, and opening /dev/tty failed: %v", err)
}
defer tty.Close()
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
fd = int(tty.Fd())
}
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()))
defer fmt.Fprintf(os.Stderr, "\n")
p, err := terminal.ReadPassword(fd)
if err != nil {
return nil, err
}
return p, nil
}

View File

@@ -12,12 +12,12 @@ 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"
)
@@ -25,14 +25,6 @@ import (
// 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"):
@@ -41,7 +33,8 @@ func parseRecipient(arg string) (age.Recipient, error) {
return agessh.ParseRecipient(arg)
case strings.HasPrefix(arg, "github:"):
name := strings.TrimPrefix(arg, "github:")
return nil, gitHubRecipientError{name}
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)
@@ -82,7 +75,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.
warningf("recipients file %q: ignoring unsupported SSH key of type %q at line %d", name, t, n)
log.Printf("Warning: 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
@@ -124,8 +117,8 @@ func sshKeyType(s string) (string, bool) {
}
// parseIdentitiesFile parses a file that contains age or SSH keys. It returns
// one or more of *age.X25519Identity, *agessh.RSAIdentity, *agessh.Ed25519Identity,
// *agessh.EncryptedSSHIdentity, or *EncryptedIdentity.
// one of *age.X25519Identity, *agessh.RSAIdentity, *agessh.Ed25519Identity, or
// *agessh.EncryptedSSHIdentity.
func parseIdentitiesFile(name string) ([]age.Identity, error) {
var f *os.File
if name == "-" {
@@ -144,40 +137,8 @@ func parseIdentitiesFile(name string) ([]age.Identity, error) {
}
b := bufio.NewReader(f)
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 pemHeader = "-----BEGIN"
if peeked, _ := b.Peek(len(pemHeader)); string(peeked) == pemHeader {
const privateKeySizeLimit = 1 << 14 // 16 KiB
contents, err := ioutil.ReadAll(io.LimitReader(b, privateKeySizeLimit))
if err != nil {
@@ -187,15 +148,13 @@ func parseIdentitiesFile(name string) ([]age.Identity, error) {
return nil, fmt.Errorf("failed to read %q: file too long", name)
}
return parseSSHIdentity(name, contents)
// 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
}
ids, err := age.ParseIdentities(b)
if err != nil {
return nil, fmt.Errorf("failed to read %q: %v", name, err)
}
return ids, nil
}
func parseSSHIdentity(name string, pemBytes []byte) ([]age.Identity, error) {
@@ -209,7 +168,8 @@ func parseSSHIdentity(name string, pemBytes []byte) ([]age.Identity, error) {
}
}
passphrasePrompt := func() ([]byte, error) {
pass, err := readPassphrase(fmt.Sprintf("Enter passphrase for %q:", name))
fmt.Fprintf(os.Stderr, "Enter passphrase for %q: ", name)
pass, err := readPassphrase()
if err != nil {
return nil, fmt.Errorf("could not read passphrase for %q: %v", name, err)
}

View File

@@ -1,5 +0,0 @@
age-encryption.org/v1
-> X25519 i6JOY3uvMdBuEybYbTp3ECFsOPEY/A3lJY1l0Qv2NC4
cD7VpfIOchU6ZjAccEjlPCNSOdJvVkxZPSf+7XS1YhY
--- 1111111111111111111111111111111111111111111
<EFBFBD>-\<5C>P9<50><39>0<1D><68><C584>Tt<54>|:٘<>#&R<>r<EFBFBD> <20><>

View File

@@ -1,6 +0,0 @@
age-encryption.org/v1
-> X25519 kx2RzHNfNuts0I131KwMCyYclZzKCGMzPUaMkH9J4z4
9qEzjtIF4NsLFnxv8EEtCwOQiXj5WHl+HWaDKNeAk+4
--- N+7l3M/ofCyzZVlPJ33CTHH8AddF0itK70QV+IIvXXA
³]Ú É+zAIÉúçê¸Ç<C2B8>éLüü“ªžà
Hˆ%Ñ¥£

View File

@@ -1,7 +1,7 @@
.\" generated with Ronn/v0.7.3
.\" http://github.com/rtomayko/ronn/tree/0.7.3
.
.TH "AGE\-KEYGEN" "1" "September 2021" "" ""
.TH "AGE\-KEYGEN" "1" "October 2021" "" ""
.
.SH "NAME"
\fBage\-keygen\fR \- generate age(1) key pairs

View File

@@ -132,7 +132,7 @@ age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
<ol class='man-decor man-foot man foot'>
<li class='tl'></li>
<li class='tc'>September 2021</li>
<li class='tc'>October 2021</li>
<li class='tr'>age-keygen(1)</li>
</ol>

View File

@@ -1,7 +1,7 @@
.\" generated with Ronn/v0.7.3
.\" http://github.com/rtomayko/ronn/tree/0.7.3
.
.TH "AGE" "1" "September 2021" "" ""
.TH "AGE" "1" "October 2021" "" ""
.
.SH "NAME"
\fBage\fR \- simple, modern, and secure file encryption
@@ -70,7 +70,7 @@ This option can be repeated and combined with \fB\-r\fR/\fB\-\-recipient\fR, and
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\.
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
@@ -105,13 +105,10 @@ Decrypt using the \fIIDENTITIES\fR at \fIPATH\fR\.
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\.
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\. 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\.
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\.
@@ -184,7 +181,7 @@ An \fBIDENTITY\fR is an SSH private key \fIfile\fR passed individually to \fB\-i
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\.
\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\.
@@ -193,7 +190,7 @@ If an error occurs during decryption, partial output might still be generated, b
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\.
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:
@@ -265,27 +262,6 @@ Enter passphrase:
.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

View File

@@ -134,7 +134,7 @@ overhead per recipient, plus 16 bytes every 64KiB of plaintext.</p>
<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
<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>
@@ -164,18 +164,12 @@ overhead per recipient, plus 16 bytes every 64KiB of plaintext.</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.
<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> d. "<code>-</code>", causing one of the options above to be read from standard input.
<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
@@ -247,7 +241,7 @@ 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
<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,
@@ -261,7 +255,7 @@ output is ever released.</p>
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>
by default. In this case, a flag will be provided to force the operation.</p>
<h2 id="EXAMPLES">EXAMPLES</h2>
@@ -302,19 +296,6 @@ $ age -d secrets.txt.age &gt; secrets.txt
Enter passphrase:
</code></pre>
<p>Encrypt and decrypt with a passphrase-protected identity file:</p>
<pre><code>$ age-keygen | age -p &gt; 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 &gt; secrets.txt.age
$ age -d -i key.age secrets.txt.age &gt; 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 &gt; example.jpg.age
@@ -338,7 +319,7 @@ $ age -d -i ~/.ssh/id_ed25519 example.jpg.age &gt; example.jpg
<ol class='man-decor man-foot man foot'>
<li class='tl'></li>
<li class='tc'>September 2021</li>
<li class='tc'>October 2021</li>
<li class='tr'>age(1)</li>
</ol>

View File

@@ -65,7 +65,7 @@ overhead per recipient, plus 16 bytes every 64KiB of plaintext.
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
This option can't be used with `-r`/`--recipient` or
`-R`/`--recipients-file`.
* `-a`, `--armor`:
@@ -97,18 +97,12 @@ overhead per recipient, plus 16 bytes every 64KiB of plaintext.
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.
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.
d\. "`-`", causing one of the options above to be read from standard input.
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
@@ -175,7 +169,7 @@ requesting its password, if any.
## EXIT STATUS
`age` will exit 0 if and only if encryption or decryption are succesful for the
`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,
@@ -189,7 +183,7 @@ Files encrypted with a stable version (not alpha, beta, or release candidate) of
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.
by default. In this case, a flag will be provided to force the operation.
## EXAMPLES
@@ -226,18 +220,6 @@ Encrypt and decrypt a file using a passphrase:
$ 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

10
go.mod
View File

@@ -1,11 +1,5 @@
module filippo.io/age
go 1.17
go 1.13
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
require golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad

24
go.sum
View File

@@ -1,14 +1,10 @@
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=
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=

View File

@@ -122,11 +122,6 @@ 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)
}