cmd/age: add -e and support encrypting with -i

This will come in handy for symmetric plugins, but make it require an
explicit -e so that missing a -d can't cause a mistaken encryption.
This commit is contained in:
Filippo Valsorda
2021-03-09 20:02:53 -05:00
committed by Filippo Valsorda
parent 801a7e8b33
commit 732f3e8a94
5 changed files with 86 additions and 13 deletions

View File

@@ -102,6 +102,13 @@ func NewRSAIdentity(key *rsa.PrivateKey) (*RSAIdentity, error) {
return i, nil
}
func (i *RSAIdentity) Recipient() age.Recipient {
return &RSARecipient{
sshKey: i.sshKey,
pubKey: &i.k.PublicKey,
}
}
func (i *RSAIdentity) Unwrap(stanzas []*age.Stanza) ([]byte, error) {
return multiUnwrap(i.unwrap, stanzas)
}
@@ -303,6 +310,13 @@ func ed25519PrivateKeyToCurve25519(pk ed25519.PrivateKey) []byte {
return out[:curve25519.ScalarSize]
}
func (i *Ed25519Identity) Recipient() age.Recipient {
return &Ed25519Recipient{
sshKey: i.sshKey,
theirPublicKey: i.ourPublicKey,
}
}
func (i *Ed25519Identity) Unwrap(stanzas []*age.Stanza) ([]byte, error) {
return multiUnwrap(i.unwrap, stanzas)
}

View File

@@ -11,6 +11,7 @@ import (
"crypto/ed25519"
"crypto/rand"
"crypto/rsa"
"reflect"
"testing"
"filippo.io/age/agessh"
@@ -36,6 +37,11 @@ func TestSSHRSARoundTrip(t *testing.T) {
t.Fatal(err)
}
// TODO: replace this with (and go-diff) with go-cmp.
if !reflect.DeepEqual(r, i.Recipient()) {
t.Fatalf("i.Recipient is different from r")
}
fileKey := make([]byte, 16)
if _, err := rand.Read(fileKey); err != nil {
t.Fatal(err)
@@ -74,6 +80,11 @@ func TestSSHEd25519RoundTrip(t *testing.T) {
t.Fatal(err)
}
// TODO: replace this with (and go-diff) with go-cmp.
if !reflect.DeepEqual(r, i.Recipient()) {
t.Fatalf("i.Recipient is different from r")
}
fileKey := make([]byte, 16)
if _, err := rand.Read(fileKey); err != nil {
t.Fatal(err)

View File

@@ -55,6 +55,10 @@ func NewEncryptedSSHIdentity(pubKey ssh.PublicKey, pemBytes []byte, passphrase f
var _ age.Identity = &EncryptedSSHIdentity{}
func (i *EncryptedSSHIdentity) Recipient() (age.Recipient, error) {
return ParseRecipient(string(ssh.MarshalAuthorizedKey(i.pubKey)))
}
// Unwrap implements age.Identity. If the private key is still encrypted, and
// any of the stanzas match the public key, it will request the passphrase. The
// decrypted private key will be cached after the first successful invocation.

View File

@@ -18,6 +18,7 @@ import (
"strings"
"filippo.io/age"
"filippo.io/age/agessh"
"filippo.io/age/armor"
"golang.org/x/crypto/ssh/terminal"
)
@@ -32,20 +33,22 @@ func (f *multiFlag) Set(value string) error {
}
const usage = `Usage:
age (-r RECIPIENT | -R PATH)... [--armor] [-o OUTPUT] [INPUT]
age --passphrase [--armor] [-o OUTPUT] [INPUT]
age [--encrypt] (-r RECIPIENT | -R PATH)... [--armor] [-o OUTPUT] [INPUT]
age [--encrypt] --passphrase [--armor] [-o OUTPUT] [INPUT]
age --decrypt [-i PATH]... [-o OUTPUT] [INPUT]
Options:
-e, --encrypt Encrypt the input to the output. Default if omitted.
-d, --decrypt Decrypt the input to the output.
-o, --output OUTPUT Write the result to the file at path OUTPUT.
-a, --armor Encrypt to a PEM encoded format.
-p, --passphrase Encrypt with a passphrase.
-r, --recipient RECIPIENT Encrypt to the specified RECIPIENT. Can be repeated.
-R, --recipients-file PATH Encrypt to recipients listed at PATH. Can be repeated.
-d, --decrypt Decrypt the input to the output.
-i, --identity PATH Use the identity file at PATH. Can be repeated.
INPUT defaults to standard input, and OUTPUT defaults to standard output.
If OUTPUT exists, it will be overwritten.
RECIPIENT can be an age public key generated by age-keygen ("age1...")
or an SSH public key ("ssh-ed25519 AAAA...", "ssh-rsa AAAA...").
@@ -59,6 +62,9 @@ one per line, or an SSH key. Empty lines and lines starting with "#" are
ignored as comments. Multiple key files can be provided, and any unused ones
will be ignored. "-" may be used to read identities from standard input.
When --encrypt is specified explicitly, -i can also be used to encrypt to an
identity file symmetrically, instead or in addition to normal recipients.
Example:
$ age-keygen -o key.txt
Public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
@@ -80,16 +86,18 @@ func main() {
}
var (
outFlag string
decryptFlag, armorFlag bool
passFlag, versionFlag bool
recipientFlags, identityFlags multiFlag
recipientsFileFlags multiFlag
outFlag string
decryptFlag, encryptFlag bool
passFlag, versionFlag, armorFlag bool
recipientFlags, identityFlags multiFlag
recipientsFileFlags multiFlag
)
flag.BoolVar(&versionFlag, "version", false, "print the version")
flag.BoolVar(&decryptFlag, "d", false, "decrypt the input")
flag.BoolVar(&decryptFlag, "decrypt", false, "decrypt the input")
flag.BoolVar(&encryptFlag, "e", false, "encrypt the input")
flag.BoolVar(&encryptFlag, "encrypt", false, "encrypt the input")
flag.BoolVar(&passFlag, "p", false, "use a passphrase")
flag.BoolVar(&passFlag, "passphrase", false, "use a passphrase")
flag.StringVar(&outFlag, "o", "", "output to `FILE` (default stdout)")
@@ -123,6 +131,9 @@ func main() {
}
switch {
case decryptFlag:
if encryptFlag {
logFatalf("Error: -e/--encrypt can't be used with -d/--decrypt.")
}
if armorFlag {
logFatalf("Error: -a/--armor can't be used with -d/--decrypt.\n" +
"Note that armored files are detected automatically.")
@@ -140,11 +151,11 @@ func main() {
"Did you mean to use -i/--identity to specify a private key?")
}
default: // encrypt
if len(identityFlags) > 0 {
logFatalf("Error: -i/--identity can't be used in encryption mode.\n" +
if len(identityFlags) > 0 && !encryptFlag {
logFatalf("Error: -i/--identity can't be used in encryption mode unless symmetric encryption is explicitly selected with -e/--encrypt.\n" +
"Did you forget to specify -d/--decrypt?")
}
if len(recipientFlags) == 0 && len(recipientsFileFlags) == 0 && !passFlag {
if len(recipientFlags)+len(recipientsFileFlags)+len(identityFlags) == 0 && !passFlag {
logFatalf("Error: missing recipients.\n" +
"Did you forget to specify -r/--recipient, -R/--recipients-file or -p/--passphrase?")
}
@@ -154,6 +165,9 @@ func main() {
if len(recipientsFileFlags) > 0 && passFlag {
logFatalf("Error: -p/--passphrase can't be combined with -R/--recipients-file.")
}
if len(identityFlags) > 0 && passFlag {
logFatalf("Error: -p/--passphrase can't be combined with -i/--identity.")
}
}
var in io.Reader = os.Stdin
@@ -202,7 +216,7 @@ func main() {
}
encryptPass(pass, in, out, armorFlag)
default:
encryptKeys(recipientFlags, recipientsFileFlags, in, out, armorFlag)
encryptKeys(recipientFlags, recipientsFileFlags, identityFlags, in, out, armorFlag)
}
}
@@ -233,7 +247,7 @@ func passphrasePromptForEncryption() (string, error) {
return p, nil
}
func encryptKeys(keys, files []string, in io.Reader, out io.Writer, armor bool) {
func encryptKeys(keys, files, identities []string, in io.Reader, out io.Writer, armor bool) {
var recipients []age.Recipient
for _, arg := range keys {
r, err := parseRecipient(arg)
@@ -249,6 +263,19 @@ func encryptKeys(keys, files []string, in io.Reader, out io.Writer, armor bool)
}
recipients = append(recipients, recs...)
}
for _, name := range identities {
ids, err := parseIdentitiesFile(name)
if err != nil {
logFatalf("Error reading %q: %v", name, err)
}
for _, id := range ids {
r, err := identityToRecipient(id)
if err != nil {
logFatalf("Internal error processing %q: %v", name, err)
}
recipients = append(recipients, r)
}
}
encrypt(recipients, in, out, armor)
}
@@ -322,6 +349,20 @@ func passphrasePrompt() (string, error) {
return string(pass), nil
}
func identityToRecipient(id age.Identity) (age.Recipient, error) {
switch id := id.(type) {
case *age.X25519Identity:
return id.Recipient(), nil
case *agessh.RSAIdentity:
return id.Recipient(), nil
case *agessh.Ed25519Identity:
return id.Recipient(), nil
case *agessh.EncryptedSSHIdentity:
return id.Recipient()
}
return nil, fmt.Errorf("unexpected identity type: %T", id)
}
type lazyOpener struct {
name string
f *os.File

View File

@@ -109,6 +109,9 @@ func sshKeyType(s string) (string, bool) {
return "", false
}
// parseIdentitiesFile parses a file that contains age or SSH keys. It returns
// one of *age.X25519Identity, *agessh.RSAIdentity, *agessh.Ed25519Identity, or
// *agessh.EncryptedSSHIdentity.
func parseIdentitiesFile(name string) ([]age.Identity, error) {
var f *os.File
if name == "-" {