mirror of
https://github.com/FiloSottile/age.git
synced 2026-01-04 19:33:55 +00:00
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:
committed by
Filippo Valsorda
parent
801a7e8b33
commit
732f3e8a94
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 == "-" {
|
||||
|
||||
Reference in New Issue
Block a user