mirror of
https://github.com/FiloSottile/age.git
synced 2026-11-08 12:26:58 +00:00
Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d4b2ae7ac | ||
|
|
1ddf01df2c | ||
|
|
f4e28fe809 | ||
|
|
0703f86521 | ||
|
|
fb97277f8d | ||
|
|
fa5b575ceb | ||
|
|
cde103daae | ||
|
|
b403e96be8 | ||
|
|
329a7ece8f | ||
|
|
3cd503dce9 | ||
|
|
cd28d56599 | ||
|
|
a94f3c3dc9 |
No files matched your search
@@ -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"
|
||||
|
||||
@@ -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,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.)
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
Vendored
+5
@@ -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.
Vendored
File renamed without changes.
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
age-encryption.org/v1
|
||||
-> X25519 kx2RzHNfNuts0I131KwMCyYclZzKCGMzPUaMkH9J4z4
|
||||
9qEzjtIF4NsLFnxv8EEtCwOQiXj5WHl+HWaDKNeAk+4
|
||||
--- N+7l3M/ofCyzZVlPJ33CTHH8AddF0itK70QV+IIvXXA
|
||||
³]Ú É+zAIÉúçê¸Ç�éLüü“ªžà
|
||||
Hˆ%Ñ¥£
|
||||
+2
-2
@@ -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
|
||||
.
|
||||
|
||||
@@ -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="mailto:age@filippo.io" data-bare-link="true">age@filippo.io</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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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 > 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
|
||||
@@ -294,12 +313,12 @@ $ age -d -i ~/.ssh/id_ed25519 example.jpg.age > example.jpg
|
||||
|
||||
<h2 id="AUTHORS">AUTHORS</h2>
|
||||
|
||||
<p>Filippo Valsorda <a href="mailto:age@filippo.io" data-bare-link="true">age@filippo.io</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
@@ -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
|
||||
|
||||
@@ -122,6 +122,11 @@ func (i *ScryptIdentity) SetMaxWorkFactor(logN int) {
|
||||
}
|
||||
|
||||
func (i *ScryptIdentity) Unwrap(stanzas []*Stanza) ([]byte, error) {
|
||||
for _, s := range stanzas {
|
||||
if s.Type == "scrypt" && len(stanzas) != 1 {
|
||||
return nil, errors.New("an scrypt recipient must be the only one")
|
||||
}
|
||||
}
|
||||
return multiUnwrap(i.unwrap, stanzas)
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user