Author SHA1 Message Date
Filippo Valsorda 9d4b2ae7ac age: move the scrypt lone recipient check out of Decrypt
The important one is the decryption side one, because when a user types
a password they expect it to both decrypt and authenticate the file.
Moved that one out of Decrypt and into ScryptIdentity, now that
Identities get all the stanzas. special_cases--

This also opens the door to other Identity implementations that do allow
multiple scrypt recipients, if someone really wants that. The CLI will
never allow it, but an explicit choice by an API consumer feels like
something we shouldn't interfere with.

Moreover, this also allows alternative Identity implementations that use
different recipient types to replicate the behavior if they have the
same authentication semantics.

The encryption side one is only a courtesy, to stop API users from
making files that won't decrypt. Unfortunately, that one needs to stay
as a special case in Encrypt, as the Recipient can't see around itself.
However, changed it to a type assertion, so custom recipients can
generate multiple scrypt recipient stanzas, if they really want.
2021-06-15 14:00:10 +02:00
GitHub Actions 1ddf01df2c doc: regenerate groff and html man pages 2021-06-14 13:24:26 +02:00
Filippo Valsorda f4e28fe809 .github/workflows: fix non-idempotent ronn email mangling 2021-06-14 13:24:26 +02:00
Filippo Valsorda 0703f86521 cmd/age,cmd/age-keygen: normalize errors, warnings, and hints 2021-06-14 13:24:26 +02:00
Filippo Valsorda fb97277f8d cmd/age: add support for encrypted identity files
Updates #252
Closes #132
2021-06-14 13:24:26 +02:00
Filippo Valsordaandcodesoap fa5b575ceb cmd/age: use CONIN$/CONOUT$ on Windows for password prompts
Fixes #128
Closes #274

Co-authored-by: codesoap <codesoap@mailbox.org>
2021-06-02 11:04:02 +02:00
GitHub Actions cde103daae doc: regenerate groff and html man pages 2021-06-01 10:26:04 +00:00
Andreas Wachowski b403e96be8 doc: fix typo in age-keygen(1) (#273) 2021-06-01 06:25:16 -04:00
GitHub Actions 329a7ece8f doc: regenerate groff and html man pages 2021-05-26 14:47:36 +00:00
Filippo Valsorda 3cd503dce9 doc: SEC 1 encoding is for ECDSA, which we don't support 2021-05-26 16:44:38 +02:00
Filippo Valsorda cd28d56599 .github/workflows: don't run ronn on new tags 2021-05-26 15:28:21 +02:00
Filippo Valsorda a94f3c3dc9 HomebrewFormula: update age to v1.0.0-rc.2 2021-05-26 13:52:31 +02:00
28 changed files with 440 additions and 158 deletions

No files matched your search

+11
View File
@@ -1,5 +1,7 @@
on:
push:
branches:
- '**'
paths:
- '**.ronn'
name: Generate man pages
@@ -13,6 +15,15 @@ jobs:
- name: Run ronn
uses: ./.github/workflows/ronn
id: ronn
- name: Undo email mangling
# rdiscount randomizes the output for no good reason, which causes
# changes to always get committed. Sigh.
# https://github.com/davidfstr/rdiscount/blob/6b1471ec3/ext/generate.c#L781-L795
run: |-
for f in doc/*.html; do
awk '/Filippo Valsorda/ { $0 = "<p>Filippo Valsorda <a href=\"mailto:age@filippo.io\" data-bare-link=\"true\">age@filippo.io</a></p>" } { print }' "$f" > "$f.tmp"
mv "$f.tmp" "$f"
done
- name: Commit and push if changed
run: |-
git config user.name "GitHub Actions"
+4 -2
View File
@@ -7,8 +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.1.zip"
sha256 "b9269bc3533fefb1dbbc90cb3683be4d4fa3ea41c1a8e7a3265415b2de26f007"
url "https://github.com/FiloSottile/age/archive/v1.0.0-rc.2.zip"
sha256 "32fa83100e660efc5ecf75547239917147798c91a277f7e1c502575b591f49da"
head "https://github.com/FiloSottile/age.git"
depends_on "go" => :build
@@ -17,5 +17,7 @@ class Age < Formula
mkdir bin
system "go", "build", "-trimpath", "-o", bin, "-ldflags", "-X main.Version=v#{version}", "filippo.io/age/cmd/..."
prefix.install_metafiles
man1.install "doc/age.1"
man1.install "doc/age-keygen.1"
end
end
+26 -4
View File
@@ -26,20 +26,22 @@ For the full documentation, read [the age(1) man page](https://htmlpreview.githu
```
Usage:
age (-r RECIPIENT | -R PATH)... [--armor] [-o OUTPUT] [INPUT]
age --passphrase [--armor] [-o OUTPUT] [INPUT]
age [--encrypt] (-r RECIPIENT | -R PATH)... [--armor] [-o OUTPUT] [INPUT]
age [--encrypt] --passphrase [--armor] [-o OUTPUT] [INPUT]
age --decrypt [-i PATH]... [-o OUTPUT] [INPUT]
Options:
-e, --encrypt Encrypt the input to the output. Default if omitted.
-d, --decrypt Decrypt the input to the output.
-o, --output OUTPUT Write the result to the file at path OUTPUT.
-a, --armor Encrypt to a PEM encoded format.
-p, --passphrase Encrypt with a passphrase.
-r, --recipient RECIPIENT Encrypt to the specified RECIPIENT. Can be repeated.
-R, --recipients-file PATH Encrypt to recipients listed at PATH. Can be repeated.
-d, --decrypt Decrypt the input to the output.
-i, --identity PATH Use the identity file at PATH. Can be repeated.
INPUT defaults to standard input, and OUTPUT defaults to standard output.
If OUTPUT exists, it will be overwritten.
RECIPIENT can be an age public key generated by age-keygen ("age1...")
or an SSH public key ("ssh-ed25519 AAAA...", "ssh-rsa AAAA...").
@@ -50,8 +52,12 @@ read recipients from standard input.
Identity files contain one or more secret keys ("AGE-SECRET-KEY-1..."),
one per line, or an SSH key. Empty lines and lines starting with "#" are
ignored as comments. Multiple key files can be provided, and any unused ones
ignored as comments. Passphrase encrypted age files can be used as
identity files. Multiple key files can be provided, and any unused ones
will be ignored. "-" may be used to read identities from standard input.
When --encrypt is specified explicitly, -i can also be used to encrypt to an
identity file symmetrically, instead or in addition to normal recipients.
```
### Multiple recipients
@@ -90,6 +96,22 @@ $ age -d secrets.txt.age > secrets.txt
Enter passphrase:
```
### Passphrase-protected key files
If an identity file passed to `-i` is a passphrase encrypted age file, it will be automatically decrypted.
```
$ age-keygen | age -p > key.age
Public key: age1yhm4gctwfmrpz87tdslm550wrx6m79y9f2hdzt0lndjnehwj0ukqrjpyx5
Enter passphrase (leave empty to autogenerate a secure one):
Using the autogenerated passphrase "hip-roast-boring-snake-mention-east-wasp-honey-input-actress".
$ age -r age1yhm4gctwfmrpz87tdslm550wrx6m79y9f2hdzt0lndjnehwj0ukqrjpyx5 secrets.txt > secrets.txt.age
$ age -d -i key.age secrets.txt.age > secrets.txt
Enter passphrase for identity file "key.age":
```
Passphrase-protected identity files are not necessary for most use cases, where access to the encrypted identity file implies access to the whole system. However, they can be useful if the identity file is stored remotely.
### SSH keys
As a convenience feature, age also supports encrypting to `ssh-rsa` and `ssh-ed25519` SSH public keys, and decrypting with the respective private key file. (`ssh-agent` is not supported.)
+10 -11
View File
@@ -103,6 +103,16 @@ func Encrypt(dst io.Writer, recipients ...Recipient) (io.WriteCloser, error) {
return nil, errors.New("no recipients specified")
}
// As a best effort, prevent an API user from generating a file that the
// ScryptIdentity will refuse to decrypt. This check can't unfortunately be
// implemented as part of the Recipient interface, so it lives as a special
// case in Encrypt.
for _, r := range recipients {
if _, ok := r.(*ScryptRecipient); ok && len(recipients) != 1 {
return nil, errors.New("an ScryptRecipient must be the only one for the file")
}
}
fileKey := make([]byte, fileKeySize)
if _, err := rand.Read(fileKey); err != nil {
return nil, err
@@ -118,11 +128,6 @@ func Encrypt(dst io.Writer, recipients ...Recipient) (io.WriteCloser, error) {
hdr.Recipients = append(hdr.Recipients, (*format.Stanza)(s))
}
}
for _, s := range hdr.Recipients {
if s.Type == "scrypt" && len(hdr.Recipients) != 1 {
return nil, errors.New("an scrypt recipient must be the only one")
}
}
if mac, err := headerMAC(fileKey, hdr); err != nil {
return nil, fmt.Errorf("failed to compute header MAC: %v", err)
} else {
@@ -169,12 +174,6 @@ 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))
+1 -1
View File
@@ -192,7 +192,7 @@ func ParseRecipient(s string) (age.Recipient, error) {
func ed25519PublicKeyToCurve25519(pk ed25519.PublicKey) ([]byte, error) {
// See https://blog.filippo.io/using-ed25519-keys-for-encryption and
// https://pkg.go.dev/filippo.io/edwards25519#Point.BytesMontgomery.
p, err := (&edwards25519.Point{}).SetBytes(pk)
p, err := new(edwards25519.Point).SetBytes(pk)
if err != nil {
return nil, err
}
+22 -9
View File
@@ -24,6 +24,7 @@ import (
// pass the result to NewEd25519Identity or NewRSAIdentity.
type EncryptedSSHIdentity struct {
pubKey ssh.PublicKey
recipient age.Recipient
pemBytes []byte
passphrase func() ([]byte, error)
@@ -41,22 +42,34 @@ type EncryptedSSHIdentity struct {
// passphrase is a callback that will be invoked by Unwrap when the passphrase
// is necessary.
func NewEncryptedSSHIdentity(pubKey ssh.PublicKey, pemBytes []byte, passphrase func() ([]byte, error)) (*EncryptedSSHIdentity, error) {
switch t := pubKey.Type(); t {
case "ssh-ed25519", "ssh-rsa":
default:
return nil, fmt.Errorf("unsupported SSH key type: %v", t)
}
return &EncryptedSSHIdentity{
i := &EncryptedSSHIdentity{
pubKey: pubKey,
pemBytes: pemBytes,
passphrase: passphrase,
}, nil
}
switch t := pubKey.Type(); t {
case "ssh-ed25519":
r, err := NewEd25519Recipient(pubKey)
if err != nil {
return nil, err
}
i.recipient = r
case "ssh-rsa":
r, err := NewRSARecipient(pubKey)
if err != nil {
return nil, err
}
i.recipient = r
default:
return nil, fmt.Errorf("unsupported SSH key type: %v", t)
}
return i, nil
}
var _ age.Identity = &EncryptedSSHIdentity{}
func (i *EncryptedSSHIdentity) Recipient() (age.Recipient, error) {
return ParseRecipient(string(ssh.MarshalAuthorizedKey(i.pubKey)))
func (i *EncryptedSSHIdentity) Recipient() age.Recipient {
return i.recipient
}
// Unwrap implements age.Identity. If the private key is still encrypted, and
+18 -10
View File
@@ -70,10 +70,10 @@ func main() {
flag.StringVar(&outFlag, "output", "", "output to `FILE` (default stdout)")
flag.Parse()
if len(flag.Args()) != 0 && !convertFlag {
log.Fatalf("age-keygen takes no arguments")
errorf("too many arguments")
}
if len(flag.Args()) > 1 && convertFlag {
log.Fatalf("Too many arguments")
errorf("too many arguments")
}
if versionFlag {
if Version != "" {
@@ -92,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 {
log.Fatalf("Failed to open output file %q: %v", outFlag, err)
errorf("failed to open output file %q: %v", outFlag, err)
}
defer func() {
if err := f.Close(); err != nil {
log.Fatalf("Failed to close output file %q: %v", outFlag, err)
errorf("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 {
log.Fatalf("Failed to open input file %q: %v", inFile, err)
errorf("failed to open input file %q: %v", inFile, err)
}
defer f.Close()
in = f
@@ -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 {
fmt.Fprintf(os.Stderr, "Warning: writing secret key to a world-readable file.\n")
warning("writing secret key to a world-readable file")
}
generate(out)
}
@@ -125,7 +125,7 @@ func main() {
func generate(out *os.File) {
k, err := age.GenerateX25519Identity()
if err != nil {
log.Fatalf("Internal error: %v", err)
errorf("internal error: %v", err)
}
if !term.IsTerminal(int(out.Fd())) {
@@ -140,16 +140,24 @@ func generate(out *os.File) {
func convert(in io.Reader, out io.Writer) {
ids, err := age.ParseIdentities(in)
if err != nil {
log.Fatalf("Failed to parse input: %v", err)
errorf("failed to parse input: %v", err)
}
if len(ids) == 0 {
log.Fatalf("No identities found in the input")
errorf("no identities found in the input")
}
for _, id := range ids {
id, ok := id.(*age.X25519Identity)
if !ok {
log.Fatalf("Internal error: unexpected identity type: %T", id)
errorf("internal error: unexpected identity type: %T", id)
}
fmt.Fprintf(out, "%s\n", id.Recipient())
}
}
func errorf(format string, v ...interface{}) {
log.Printf("age-keygen: error: "+format, v...)
}
func warning(msg string) {
log.Printf("age-keygen: warning: " + msg)
}
+89 -64
View File
@@ -12,7 +12,7 @@ import (
"flag"
"fmt"
"io"
_log "log"
"log"
"os"
"runtime/debug"
"strings"
@@ -59,7 +59,8 @@ read recipients from standard input.
Identity files contain one or more secret keys ("AGE-SECRET-KEY-1..."),
one per line, or an SSH key. Empty lines and lines starting with "#" are
ignored as comments. Multiple key files can be provided, and any unused ones
ignored as comments. Passphrase encrypted age files can be used as
identity files. Multiple key files can be provided, and any unused ones
will be ignored. "-" may be used to read identities from standard input.
When --encrypt is specified explicitly, -i can also be used to encrypt to an
@@ -77,7 +78,7 @@ Example:
var Version string
func main() {
_log.SetFlags(0)
log.SetFlags(0)
flag.Usage = func() { fmt.Fprintf(os.Stderr, "%s\n", usage) }
if len(os.Args) == 1 {
@@ -126,47 +127,47 @@ func main() {
}
if flag.NArg() > 1 {
logFatalf("Error: too many arguments: %q.\n"+
"Note that the input file must be specified after all flags.", flag.Args())
errorWithHint(fmt.Sprintf("too many arguments: %q", flag.Args()),
"note that the input file must be specified after all flags")
}
switch {
case decryptFlag:
if encryptFlag {
logFatalf("Error: -e/--encrypt can't be used with -d/--decrypt.")
errorf("-e/--encrypt can't be used with -d/--decrypt")
}
if armorFlag {
logFatalf("Error: -a/--armor can't be used with -d/--decrypt.\n" +
"Note that armored files are detected automatically.")
errorWithHint("-a/--armor can't be used with -d/--decrypt",
"note that armored files are detected automatically")
}
if passFlag {
logFatalf("Error: -p/--passphrase can't be used with -d/--decrypt.\n" +
"Note that password protected files are detected automatically.")
errorWithHint("-p/--passphrase can't be used with -d/--decrypt",
"note that password protected files are detected automatically")
}
if len(recipientFlags) > 0 {
logFatalf("Error: -r/--recipient can't be used with -d/--decrypt.\n" +
"Did you mean to use -i/--identity to specify a private key?")
errorWithHint("-r/--recipient can't be used with -d/--decrypt",
"did you mean to use -i/--identity to specify a private key?")
}
if len(recipientsFileFlags) > 0 {
logFatalf("Error: -R/--recipients-file can't be used with -d/--decrypt.\n" +
"Did you mean to use -i/--identity to specify a private key?")
errorWithHint("-R/--recipients-file can't be used with -d/--decrypt",
"did you mean to use -i/--identity to specify a private key?")
}
default: // encrypt
if len(identityFlags) > 0 && !encryptFlag {
logFatalf("Error: -i/--identity can't be used in encryption mode unless symmetric encryption is explicitly selected with -e/--encrypt.\n" +
"Did you forget to specify -d/--decrypt?")
errorWithHint("-i/--identity can't be used in encryption mode unless symmetric encryption is explicitly selected with -e/--encrypt",
"did you forget to specify -d/--decrypt?")
}
if len(recipientFlags)+len(recipientsFileFlags)+len(identityFlags) == 0 && !passFlag {
logFatalf("Error: missing recipients.\n" +
"Did you forget to specify -r/--recipient, -R/--recipients-file or -p/--passphrase?")
errorWithHint("missing recipients",
"did you forget to specify -r/--recipient, -R/--recipients-file or -p/--passphrase?")
}
if len(recipientFlags) > 0 && passFlag {
logFatalf("Error: -p/--passphrase can't be combined with -r/--recipient.")
errorf("-p/--passphrase can't be combined with -r/--recipient")
}
if len(recipientsFileFlags) > 0 && passFlag {
logFatalf("Error: -p/--passphrase can't be combined with -R/--recipients-file.")
errorf("-p/--passphrase can't be combined with -R/--recipients-file")
}
if len(identityFlags) > 0 && passFlag {
logFatalf("Error: -p/--passphrase can't be combined with -i/--identity.")
errorf("-p/--passphrase can't be combined with -i/--identity")
}
}
@@ -175,7 +176,7 @@ func main() {
if name := flag.Arg(0); name != "" && name != "-" {
f, err := os.Open(name)
if err != nil {
logFatalf("Error: failed to open input file %q: %v", name, err)
errorf("failed to open input file %q: %v", name, err)
}
defer f.Close()
in = f
@@ -186,7 +187,7 @@ func main() {
f := newLazyOpener(name)
defer func() {
if err := f.Close(); err != nil {
logFatalf("Error: failed to close output file %q: %v", name, err)
errorf("failed to close output file %q: %v", name, err)
}
}()
out = f
@@ -197,8 +198,9 @@ func main() {
} else if !armorFlag {
// If the output wouldn't be armored, refuse to send binary to
// the terminal unless explicitly requested with "-o -".
logFatalf("Error: refusing to output binary to the terminal.\n" +
`Did you mean to use -a/--armor? Force with "-o -".`)
errorWithHint("refusing to output binary to the terminal",
"did you mean to use -a/--armor?",
`force anyway with "-o -"`)
}
}
if in == os.Stdin && term.IsTerminal(int(os.Stdin.Fd())) {
@@ -216,7 +218,7 @@ func main() {
case passFlag:
pass, err := passphrasePromptForEncryption()
if err != nil {
logFatalf("Error: %v", err)
errorf("%v", err)
}
encryptPass(pass, in, out, armorFlag)
default:
@@ -225,8 +227,7 @@ func main() {
}
func passphrasePromptForEncryption() (string, error) {
fmt.Fprintf(os.Stderr, "Enter passphrase (leave empty to autogenerate a secure one): ")
pass, err := readPassphrase()
pass, err := readPassphrase("Enter passphrase (leave empty to autogenerate a secure one):")
if err != nil {
return "", fmt.Errorf("could not read passphrase: %v", err)
}
@@ -237,10 +238,10 @@ func passphrasePromptForEncryption() (string, error) {
words = append(words, randomWord())
}
p = strings.Join(words, "-")
// TODO: consider printing this to the terminal, instead of stderr.
fmt.Fprintf(os.Stderr, "Using the autogenerated passphrase %q.\n", p)
} else {
fmt.Fprintf(os.Stderr, "Confirm passphrase: ")
confirm, err := readPassphrase()
confirm, err := readPassphrase("Confirm passphrase:")
if err != nil {
return "", fmt.Errorf("could not read passphrase: %v", err)
}
@@ -255,30 +256,33 @@ func encryptKeys(keys, files, identities []string, in io.Reader, out io.Writer,
var recipients []age.Recipient
for _, arg := range keys {
r, err := parseRecipient(arg)
if err, ok := err.(gitHubRecipientError); ok {
errorWithHint(err.Error(), "instead, use recipient files like",
" curl -O https://github.com/"+err.username+".keys",
" age -R "+err.username+".keys")
}
if err != nil {
logFatalf("Error: %v", err)
errorf("%v", err)
}
recipients = append(recipients, r)
}
for _, name := range files {
recs, err := parseRecipientsFile(name)
if err != nil {
logFatalf("Error: failed to parse recipient file %q: %v", name, err)
errorf("failed to parse recipient file %q: %v", name, err)
}
recipients = append(recipients, recs...)
}
for _, name := range identities {
ids, err := parseIdentitiesFile(name)
if err != nil {
logFatalf("Error reading %q: %v", name, err)
errorf("reading %q: %v", name, err)
}
for _, id := range ids {
r, err := identityToRecipient(id)
if err != nil {
logFatalf("Internal error processing %q: %v", name, err)
}
recipients = append(recipients, r)
r, err := identitiesToRecipients(ids)
if err != nil {
errorf("internal error processing %q: %v", name, err)
}
recipients = append(recipients, r...)
}
encrypt(recipients, in, out, armor)
}
@@ -286,7 +290,7 @@ func encryptKeys(keys, files, identities []string, in io.Reader, out io.Writer,
func encryptPass(pass string, in io.Reader, out io.Writer, armor bool) {
r, err := age.NewScryptRecipient(pass)
if err != nil {
logFatalf("Error: %v", err)
errorf("%v", err)
}
encrypt([]age.Recipient{r}, in, out, armor)
}
@@ -296,20 +300,20 @@ func encrypt(recipients []age.Recipient, in io.Reader, out io.Writer, withArmor
a := armor.NewWriter(out)
defer func() {
if err := a.Close(); err != nil {
logFatalf("Error: %v", err)
errorf("%v", err)
}
}()
out = a
}
w, err := age.Encrypt(out, recipients...)
if err != nil {
logFatalf("Error: %v", err)
errorf("%v", err)
}
if _, err := io.Copy(w, in); err != nil {
logFatalf("Error: %v", err)
errorf("%v", err)
}
if err := w.Close(); err != nil {
logFatalf("Error: %v", err)
errorf("%v", err)
}
}
@@ -323,7 +327,7 @@ func decrypt(keys []string, in io.Reader, out io.Writer) {
for _, name := range keys {
ids, err := parseIdentitiesFile(name)
if err != nil {
logFatalf("Error reading %q: %v", name, err)
errorf("reading %q: %v", name, err)
}
identities = append(identities, ids...)
}
@@ -337,34 +341,44 @@ func decrypt(keys []string, in io.Reader, out io.Writer) {
r, err := age.Decrypt(in, identities...)
if err != nil {
logFatalf("Error: %v", err)
errorf("%v", err)
}
if _, err := io.Copy(out, r); err != nil {
logFatalf("Error: %v", err)
errorf("%v", err)
}
}
func passphrasePrompt() (string, error) {
fmt.Fprintf(os.Stderr, "Enter passphrase: ")
pass, err := readPassphrase()
pass, err := readPassphrase("Enter passphrase:")
if err != nil {
return "", fmt.Errorf("could not read passphrase: %v", err)
}
return string(pass), nil
}
func identityToRecipient(id age.Identity) (age.Recipient, error) {
switch id := id.(type) {
case *age.X25519Identity:
return id.Recipient(), nil
case *agessh.RSAIdentity:
return id.Recipient(), nil
case *agessh.Ed25519Identity:
return id.Recipient(), nil
case *agessh.EncryptedSSHIdentity:
return id.Recipient()
func identitiesToRecipients(ids []age.Identity) ([]age.Recipient, error) {
var recipients []age.Recipient
for _, id := range ids {
switch id := id.(type) {
case *age.X25519Identity:
recipients = append(recipients, id.Recipient())
case *agessh.RSAIdentity:
recipients = append(recipients, id.Recipient())
case *agessh.Ed25519Identity:
recipients = append(recipients, id.Recipient())
case *agessh.EncryptedSSHIdentity:
recipients = append(recipients, id.Recipient())
case *EncryptedIdentity:
r, err := id.Recipients()
if err != nil {
return nil, err
}
recipients = append(recipients, r...)
default:
return nil, fmt.Errorf("unexpected identity type: %T", id)
}
}
return nil, fmt.Errorf("unexpected identity type: %T", id)
return recipients, nil
}
type lazyOpener struct {
@@ -394,8 +408,19 @@ func (l *lazyOpener) Close() error {
return nil
}
func logFatalf(format string, v ...interface{}) {
_log.Printf(format, v...)
_log.Fatalf("[ Did age not do what you expected? Could an error be more useful?" +
" Tell us: https://filippo.io/age/report ]")
func errorf(format string, v ...interface{}) {
log.Printf("age: error: "+format, v...)
log.Fatalf("age: report unexpected or unhelpful errors at https://filippo.io/age/report")
}
func warningf(format string, v ...interface{}) {
log.Printf("age: warning: "+format, v...)
}
func errorWithHint(error string, hints ...string) {
log.Printf("age: error: %s", error)
for _, hint := range hints {
log.Printf("age: hint: %s", hint)
}
log.Fatalf("age: report unexpected or unhelpful errors at https://filippo.io/age/report")
}
+20 -12
View File
@@ -18,24 +18,30 @@ import (
)
func TestVectors(t *testing.T) {
defaultIDs, err := parseIdentitiesFile("testdata/default_key.txt")
var defaultIDs []age.Identity
password, err := ioutil.ReadFile("testdata/default_password.txt")
if err != nil {
t.Fatal(err)
}
password, err := ioutil.ReadFile("testdata/default_password.txt")
if err == nil {
p := strings.TrimSpace(string(password))
i, err := age.NewScryptIdentity(p)
if err != nil {
t.Fatal(err)
}
defaultIDs = append(defaultIDs, i)
p := strings.TrimSpace(string(password))
i, err := age.NewScryptIdentity(p)
if err != nil {
t.Fatal(err)
}
defaultIDs = append(defaultIDs, i)
ids, err := parseIdentitiesFile("testdata/default_key.txt")
if err != nil {
t.Fatal(err)
}
defaultIDs = append(defaultIDs, ids...)
files, _ := filepath.Glob("testdata/*.age")
for _, f := range files {
_, name := filepath.Split(f)
name = strings.TrimSuffix(name, ".age")
expectPass := strings.HasPrefix(name, "good_")
expectFailure := strings.HasPrefix(name, "fail_")
expectNoMatch := strings.HasPrefix(name, "nomatch_")
t.Run(name, func(t *testing.T) {
@@ -63,17 +69,17 @@ func TestVectors(t *testing.T) {
if err == nil {
t.Fatal("expected Decrypt failure")
}
if e := (&age.NoIdentityMatchError{}); errors.As(err, &e) {
if e := new(age.NoIdentityMatchError); errors.As(err, &e) {
t.Errorf("got ErrIncorrectIdentity, expected more specific error")
}
} else if expectNoMatch {
if err == nil {
t.Fatal("expected Decrypt failure")
}
if e := (&age.NoIdentityMatchError{}); !errors.As(err, &e) {
if e := new(age.NoIdentityMatchError); !errors.As(err, &e) {
t.Errorf("expected ErrIncorrectIdentity, got %v", err)
}
} else {
} else if expectPass {
if err != nil {
t.Fatal(err)
}
@@ -82,6 +88,8 @@ func TestVectors(t *testing.T) {
t.Fatal(err)
}
t.Logf("%s", out)
} else {
t.Fatal("invalid test vector")
}
})
}
+92 -15
View File
@@ -7,9 +7,11 @@
package main
import (
"bytes"
"errors"
"fmt"
"os"
"runtime"
"filippo.io/age"
"golang.org/x/term"
@@ -22,6 +24,11 @@ type LazyScryptIdentity struct {
var _ age.Identity = &LazyScryptIdentity{}
func (i *LazyScryptIdentity) Unwrap(stanzas []*age.Stanza) (fileKey []byte, err error) {
for _, s := range stanzas {
if s.Type == "scrypt" && len(stanzas) != 1 {
return nil, errors.New("an scrypt recipient must be the only one")
}
}
if len(stanzas) != 1 || stanzas[0].Type != "scrypt" {
return nil, age.ErrIncorrectIdentity
}
@@ -45,23 +52,93 @@ func (i *LazyScryptIdentity) Unwrap(stanzas []*age.Stanza) (fileKey []byte, err
return fileKey, err
}
// 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 !term.IsTerminal(fd) {
tty, err := os.Open("/dev/tty")
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
}
if err != nil {
return nil, fmt.Errorf("standard input is not a terminal, and opening /dev/tty failed: %v", err)
return nil, err
}
return fileKey, nil
}
i.NoMatchWarning()
return nil, age.ErrIncorrectIdentity
}
func (i *EncryptedIdentity) decrypt() error {
d, err := age.Decrypt(bytes.NewReader(i.Contents), &LazyScryptIdentity{i.Passphrase})
if e := new(age.NoIdentityMatchError); errors.As(err, &e) {
return fmt.Errorf("identity file is encrypted with age but not with a passphrase")
}
if err != nil {
return fmt.Errorf("failed to decrypt identity file: %v", err)
}
i.identities, err = age.ParseIdentities(d)
return err
}
// readPassphrase reads a passphrase from the terminal. It does not read from a
// non-terminal stdin, so it does not check stdinInUse.
func readPassphrase(prompt string) ([]byte, error) {
var in, out *os.File
if runtime.GOOS == "windows" {
var err error
in, err = os.OpenFile("CONIN$", os.O_RDWR, 0)
if err != nil {
return nil, err
}
defer in.Close()
out, err = os.OpenFile("CONOUT$", os.O_WRONLY, 0)
if err != nil {
return nil, err
}
defer out.Close()
} else if _, err := os.Stat("/dev/tty"); err == nil {
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
if err != nil {
return nil, err
}
defer tty.Close()
fd = int(tty.Fd())
in, out = tty, tty
} else {
if !term.IsTerminal(int(os.Stdin.Fd())) {
return nil, fmt.Errorf("standard input is not a terminal, and /dev/tty is not available: %v", err)
}
in, out = os.Stdin, os.Stderr
}
defer fmt.Fprintf(os.Stderr, "\n")
p, err := term.ReadPassword(fd)
if err != nil {
return nil, err
}
return p, nil
fmt.Fprintf(out, "%s ", prompt)
// Use CRLF to work around an apparent bug in WSL2's handling of CONOUT$.
// Only when running a Windows binary from WSL2, the cursor would not go
// back to the start of the line with a simple LF. Honestly, it's impressive
// CONIN$ and CONOUT$ even work at all inside WSL2.
defer fmt.Fprintf(out, "\r\n")
return term.ReadPassword(int(in.Fd()))
}
+55 -15
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,6 +25,14 @@ 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"):
@@ -33,8 +41,7 @@ func parseRecipient(arg string) (age.Recipient, error) {
return agessh.ParseRecipient(arg)
case strings.HasPrefix(arg, "github:"):
name := strings.TrimPrefix(arg, "github:")
return nil, fmt.Errorf(`"github:" recipients were removed from the design.`+"\n"+
"Instead, use recipient files like\n\n curl -O https://github.com/%s.keys\n age -R %s.keys\n\n", name, name)
return nil, gitHubRecipientError{name}
}
return nil, fmt.Errorf("unknown recipient type: %q", arg)
@@ -75,7 +82,7 @@ func parseRecipientsFile(name string) ([]age.Recipient, error) {
if err != nil {
if t, ok := sshKeyType(line); ok {
// Skip unsupported but valid SSH public keys with a warning.
log.Printf("Warning: recipients file %q: ignoring unsupported SSH key of type %q at line %d", name, t, n)
warningf("recipients file %q: ignoring unsupported SSH key of type %q at line %d", name, t, n)
continue
}
// Hide the error since it might unintentionally leak the contents
@@ -117,8 +124,8 @@ func sshKeyType(s string) (string, bool) {
}
// parseIdentitiesFile parses a file that contains age or SSH keys. It returns
// one of *age.X25519Identity, *agessh.RSAIdentity, *agessh.Ed25519Identity, or
// *agessh.EncryptedSSHIdentity.
// one or more of *age.X25519Identity, *agessh.RSAIdentity, *agessh.Ed25519Identity,
// *agessh.EncryptedSSHIdentity, or *EncryptedIdentity.
func parseIdentitiesFile(name string) ([]age.Identity, error) {
var f *os.File
if name == "-" {
@@ -137,8 +144,40 @@ func parseIdentitiesFile(name string) ([]age.Identity, error) {
}
b := bufio.NewReader(f)
const pemHeader = "-----BEGIN"
if peeked, _ := b.Peek(len(pemHeader)); string(peeked) == pemHeader {
p, _ := b.Peek(14) // length of "age-encryption" and "-----BEGIN AGE"
peeked := string(p)
switch {
// An age encrypted file, plain or armored.
case peeked == "age-encryption" || peeked == "-----BEGIN AGE":
var r io.Reader = b
if peeked == "-----BEGIN AGE" {
r = armor.NewReader(r)
}
const privateKeySizeLimit = 1 << 24 // 16 MiB
contents, err := ioutil.ReadAll(io.LimitReader(r, privateKeySizeLimit))
if err != nil {
return nil, fmt.Errorf("failed to read %q: %v", name, err)
}
if len(contents) == privateKeySizeLimit {
return nil, fmt.Errorf("failed to read %q: file too long", name)
}
return []age.Identity{&EncryptedIdentity{
Contents: contents,
Passphrase: func() (string, error) {
pass, err := readPassphrase(fmt.Sprintf("Enter passphrase for identity file %q:", name))
if err != nil {
return "", fmt.Errorf("could not read passphrase: %v", err)
}
return string(pass), nil
},
NoMatchWarning: func() {
warningf("encrypted identity file %q didn't match file's recipients", name)
},
}}, nil
// Another PEM file, possibly an SSH private key.
case strings.HasPrefix(peeked, "-----BEGIN"):
const privateKeySizeLimit = 1 << 14 // 16 KiB
contents, err := ioutil.ReadAll(io.LimitReader(b, privateKeySizeLimit))
if err != nil {
@@ -148,13 +187,15 @@ func parseIdentitiesFile(name string) ([]age.Identity, error) {
return nil, fmt.Errorf("failed to read %q: file too long", name)
}
return parseSSHIdentity(name, contents)
}
ids, err := age.ParseIdentities(b)
if err != nil {
return nil, fmt.Errorf("failed to read %q: %v", name, err)
// An unencrypted age identity file.
default:
ids, err := age.ParseIdentities(b)
if err != nil {
return nil, fmt.Errorf("failed to read %q: %v", name, err)
}
return ids, nil
}
return ids, nil
}
func parseSSHIdentity(name string, pemBytes []byte) ([]age.Identity, error) {
@@ -168,8 +209,7 @@ func parseSSHIdentity(name string, pemBytes []byte) ([]age.Identity, error) {
}
}
passphrasePrompt := func() ([]byte, error) {
fmt.Fprintf(os.Stderr, "Enter passphrase for %q: ", name)
pass, err := readPassphrase()
pass, err := readPassphrase(fmt.Sprintf("Enter passphrase for %q:", name))
if err != nil {
return nil, fmt.Errorf("could not read passphrase for %q: %v", name, err)
}
+5
View File
@@ -0,0 +1,5 @@
age-encryption.org/v1
-> X25519 i6JOY3uvMdBuEybYbTp3ECFsOPEY/A3lJY1l0Qv2NC4
cD7VpfIOchU6ZjAccEjlPCNSOdJvVkxZPSf+7XS1YhY
--- 1111111111111111111111111111111111111111111
�-\�P9��0�hń��Tt�|:٘�#&R�r� ��
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+6
View File
@@ -0,0 +1,6 @@
age-encryption.org/v1
-> X25519 kx2RzHNfNuts0I131KwMCyYclZzKCGMzPUaMkH9J4z4
9qEzjtIF4NsLFnxv8EEtCwOQiXj5WHl+HWaDKNeAk+4
--- N+7l3M/ofCyzZVlPJ33CTHH8AddF0itK70QV+IIvXXA
³]Ú É+zAIÉúçê¸Ç�éLüü“ªžà
Hˆ%Ñ¥£
+2 -2
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" "May 2021" "" ""
.TH "AGE\-KEYGEN" "1" "June 2021" "" ""
.
.SH "NAME"
\fBage\-keygen\fR \- generate age(1) key pairs
@@ -68,7 +68,7 @@ Public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
.IP "" 0
.
.P
Convert an identity to a receipient:
Convert an identity to a recipient:
.
.IP "" 4
.
+3 -3
View File
@@ -115,7 +115,7 @@ AGE-SECRET-KEY-1N9JEPW6DWJ0ZQUDX63F5A03GX8QUW7PXDE39N8UYF82VZ9PC8UFS3M7XA9
Public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
</code></pre>
<p>Convert an identity to a receipient:</p>
<p>Convert an identity to a recipient:</p>
<pre><code>$ age-keygen -y key.txt
age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
@@ -127,12 +127,12 @@ age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
<h2 id="AUTHORS">AUTHORS</h2>
<p>Filippo Valsorda <a href="&#x6d;&#97;&#105;&#x6c;&#x74;&#111;&#x3a;&#x61;&#103;&#101;&#x40;&#x66;&#x69;&#x6c;&#x69;&#112;&#x70;&#111;&#x2e;&#x69;&#111;" data-bare-link="true">&#x61;&#x67;&#x65;&#64;&#x66;&#x69;&#x6c;&#x69;&#112;&#112;&#111;&#46;&#105;&#x6f;</a></p>
<p>Filippo Valsorda <a href="mailto:age@filippo.io" data-bare-link="true">age@filippo.io</a></p>
<ol class='man-decor man-foot man foot'>
<li class='tl'></li>
<li class='tc'>May 2021</li>
<li class='tc'>June 2021</li>
<li class='tr'>age-keygen(1)</li>
</ol>
+1 -1
View File
@@ -43,7 +43,7 @@ Write a new identity to `key.txt`:
$ age-keygen -o key.txt
Public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
Convert an identity to a receipient:
Convert an identity to a recipient:
$ age-keygen -y key.txt
age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
+27 -3
View File
@@ -1,7 +1,7 @@
.\" generated with Ronn/v0.7.3
.\" http://github.com/rtomayko/ronn/tree/0.7.3
.
.TH "AGE" "1" "May 2021" "" ""
.TH "AGE" "1" "June 2021" "" ""
.
.SH "NAME"
\fBage\fR \- simple, modern, and secure file encryption
@@ -105,10 +105,13 @@ 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\. An SSH private key file, in PKCS#1, PKCS#8, SEC 1, 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\.
b\. A passphrase encrypted age file, containing \fIIDENTITIES\fR one per line like above\. The passphrase is requested interactively\. Note that passphrase\-protected identity files are not necessary for most use cases, where access to the encrypted identity file implies access to the whole system\.
.
.IP
c\. "\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\. An SSH private key file, in PKCS#1, PKCS#8, or OpenSSH format\. If the private key is password\-protected, the password is requested interactively only if the SSH identity matches the file\. See the \fISSH keys\fR section for more information, including supported key types\.
.
.IP
d\. "\fB\-\fR", causing one of the options above to be read from standard input\. In this case, the \fIINPUT\fR argument must be specified\.
.
.IP
This option can be repeated\. Identities are tried in the order in which are provided, and the first one matching one of the file\'s recipients is used\. Unused identities are ignored\.
@@ -250,6 +253,27 @@ 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
+23 -4
View File
@@ -162,12 +162,18 @@ 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. An SSH private key file, in PKCS#1, PKCS#8, SEC 1, or OpenSSH format.
<p> b. A passphrase encrypted age file, containing
<a href="#RECIPIENTS-AND-IDENTITIES" title="RECIPIENTS AND IDENTITIES" data-bare-link="true">IDENTITIES</a> one per line like above.
The passphrase is requested interactively. Note that passphrase-protected
identity files are not necessary for most use cases, where access to the
encrypted identity file implies access to the whole system.</p>
<p> c. An SSH private key file, in PKCS#1, PKCS#8, or OpenSSH format.
If the private key is password-protected, the password is requested
interactively only if the SSH identity matches the file. See the
<a href="#SSH-keys" title="SSH keys" data-bare-link="true">SSH keys</a> section for more information, including supported key types.</p>
<p> c. "<code>-</code>", causing one of the options above to be read from standard input.
<p> d. "<code>-</code>", causing one of the options above to be read from standard input.
In this case, the <var>INPUT</var> argument must be specified.</p>
<p> This option can be repeated. Identities are tried in the order in which
@@ -276,6 +282,19 @@ $ 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
@@ -294,12 +313,12 @@ $ age -d -i ~/.ssh/id_ed25519 example.jpg.age &gt; example.jpg
<h2 id="AUTHORS">AUTHORS</h2>
<p>Filippo Valsorda <a href="&#x6d;&#97;&#105;&#x6c;&#116;&#x6f;&#58;&#x61;&#103;&#x65;&#64;&#102;&#105;&#x6c;&#105;&#x70;&#x70;&#x6f;&#46;&#105;&#111;" data-bare-link="true">&#97;&#x67;&#x65;&#x40;&#x66;&#x69;&#108;&#105;&#x70;&#x70;&#x6f;&#x2e;&#105;&#111;</a></p>
<p>Filippo Valsorda <a href="mailto:age@filippo.io" data-bare-link="true">age@filippo.io</a></p>
<ol class='man-decor man-foot man foot'>
<li class='tl'></li>
<li class='tc'>May 2021</li>
<li class='tc'>June 2021</li>
<li class='tr'>age(1)</li>
</ol>
+20 -2
View File
@@ -97,12 +97,18 @@ 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\. An SSH private key file, in PKCS#1, PKCS#8, SEC 1, or OpenSSH format.
b\. A passphrase encrypted age file, containing
[IDENTITIES][RECIPIENTS AND IDENTITIES] one per line like above.
The passphrase is requested interactively. Note that passphrase-protected
identity files are not necessary for most use cases, where access to the
encrypted identity file implies access to the whole system.
c\. An SSH private key file, in PKCS#1, PKCS#8, or OpenSSH format.
If the private key is password-protected, the password is requested
interactively only if the SSH identity matches the file. See the
[SSH keys][] section for more information, including supported key types.
c\. "`-`", causing one of the options above to be read from standard input.
d\. "`-`", causing one of the options above to be read from standard input.
In this case, the <INPUT> argument must be specified.
This option can be repeated. Identities are tried in the order in which
@@ -202,6 +208,18 @@ 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
+5
View File
@@ -122,6 +122,11 @@ func (i *ScryptIdentity) SetMaxWorkFactor(logN int) {
}
func (i *ScryptIdentity) Unwrap(stanzas []*Stanza) ([]byte, error) {
for _, s := range stanzas {
if s.Type == "scrypt" && len(stanzas) != 1 {
return nil, errors.New("an scrypt recipient must be the only one")
}
}
return multiUnwrap(i.unwrap, stanzas)
}