mirror of
https://github.com/FiloSottile/age.git
synced 2026-01-14 15:32:48 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eaf2cef49d | ||
|
|
581cff8473 | ||
|
|
40ef1b6a62 | ||
|
|
11659e8c97 | ||
|
|
7309913372 | ||
|
|
6f86a7f520 | ||
|
|
b59a9ecb5d | ||
|
|
bceb0e0423 |
4
.github/ISSUE_TEMPLATE/bug-report.md
vendored
4
.github/ISSUE_TEMPLATE/bug-report.md
vendored
@@ -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: ''
|
||||
|
||||
10
.github/ISSUE_TEMPLATE/config.yml
vendored
10
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -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
15
.github/ISSUE_TEMPLATE/spec-feedback.md
vendored
Normal 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
21
.github/ISSUE_TEMPLATE/ux-report.md
vendored
Normal 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>
|
||||
```
|
||||
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -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:
|
||||
|
||||
2
.github/workflows/gotip.yml
vendored
2
.github/workflows/gotip.yml
vendored
@@ -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:
|
||||
|
||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -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:
|
||||
|
||||
@@ -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
144
README.md
@@ -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
|
||||
|
||||
[](https://pkg.go.dev/filippo.io/age)
|
||||
[](https://htmlpreview.github.io/?https://github.com/FiloSottile/age/blob/master/doc/age.1.html)
|
||||
[](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
21
age.go
@@ -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))
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
159
cmd/age/age.go
159
cmd/age/age.go
@@ -12,7 +12,7 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
_log "log"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
"filippo.io/age"
|
||||
"filippo.io/age/agessh"
|
||||
"filippo.io/age/armor"
|
||||
"golang.org/x/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 ]")
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
5
cmd/age/testdata/fail_bad_hmac.age
vendored
5
cmd/age/testdata/fail_bad_hmac.age
vendored
@@ -1,5 +0,0 @@
|
||||
age-encryption.org/v1
|
||||
-> X25519 i6JOY3uvMdBuEybYbTp3ECFsOPEY/A3lJY1l0Qv2NC4
|
||||
cD7VpfIOchU6ZjAccEjlPCNSOdJvVkxZPSf+7XS1YhY
|
||||
--- 1111111111111111111111111111111111111111111
|
||||
<EFBFBD>-\<5C>P9<50><39>0<1D>hń<68><C584>Tt<54>|:٘<>#&R<>r<EFBFBD> <20><>
|
||||
6
cmd/age/testdata/good_simple.age
vendored
6
cmd/age/testdata/good_simple.age
vendored
@@ -1,6 +0,0 @@
|
||||
age-encryption.org/v1
|
||||
-> X25519 kx2RzHNfNuts0I131KwMCyYclZzKCGMzPUaMkH9J4z4
|
||||
9qEzjtIF4NsLFnxv8EEtCwOQiXj5WHl+HWaDKNeAk+4
|
||||
--- N+7l3M/ofCyzZVlPJ33CTHH8AddF0itK70QV+IIvXXA
|
||||
³]Ú É+zAIÉúçê¸Ç<C2B8>éLüü“ªžà
|
||||
Hˆ%Ñ¥£
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
36
doc/age.1
36
doc/age.1
@@ -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
|
||||
|
||||
@@ -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 > secrets.txt
|
||||
Enter passphrase:
|
||||
</code></pre>
|
||||
|
||||
<p>Encrypt and decrypt with a passphrase-protected identity file:</p>
|
||||
|
||||
<pre><code>$ age-keygen | age -p > key.age
|
||||
Public key: age1yhm4gctwfmrpz87tdslm550wrx6m79y9f2hdzt0lndjnehwj0ukqrjpyx5
|
||||
Enter passphrase (leave empty to autogenerate a secure one):
|
||||
Using the autogenerated passphrase "hip-roast-boring-snake-mention-east-wasp-honey-input-actress".
|
||||
|
||||
$ age -r age1yhm4gctwfmrpz87tdslm550wrx6m79y9f2hdzt0lndjnehwj0ukqrjpyx5 secrets.txt > secrets.txt.age
|
||||
|
||||
$ age -d -i key.age secrets.txt.age > secrets.txt
|
||||
Enter passphrase for identity file "key.age":
|
||||
</code></pre>
|
||||
|
||||
<p>Encrypt and decrypt with an SSH public key:</p>
|
||||
|
||||
<pre><code>$ age -R ~/.ssh/id_ed25519.pub example.jpg > example.jpg.age
|
||||
@@ -338,7 +319,7 @@ $ age -d -i ~/.ssh/id_ed25519 example.jpg.age > 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>
|
||||
|
||||
|
||||
@@ -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
10
go.mod
@@ -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
24
go.sum
@@ -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=
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user