diff --git a/cmd/age/age.go b/cmd/age/age.go index 63f792d..15b8909 100644 --- a/cmd/age/age.go +++ b/cmd/age/age.go @@ -31,13 +31,15 @@ func main() { log.SetFlags(0) var ( - outFlag string - decryptFlag, armorFlag bool - recipientFlags, identityFlags multiFlag + outFlag string + decryptFlag, armorFlag, passFlag bool + recipientFlags, identityFlags multiFlag ) flag.BoolVar(&decryptFlag, "d", false, "decrypt the input") flag.BoolVar(&decryptFlag, "decrypt", false, "decrypt 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)") flag.BoolVar(&armorFlag, "a", false, "generate an armored file") flag.BoolVar(&armorFlag, "armor", false, "generate an armored file") @@ -66,9 +68,12 @@ func main() { log.Printf("Error: -i/--identity can't be used in encryption mode.") log.Fatalf("Did you forget to specify -d/--decrypt?") } - if len(recipientFlags) == 0 { + if len(recipientFlags) == 0 && !passFlag { log.Printf("Error: missing recipients.") - log.Fatalf("Did you forget to specify -r/--recipient?") + log.Fatalf("Did you forget to specify -r/--recipient or -p/--passphrase?") + } + if len(recipientFlags) > 0 && passFlag { + log.Fatalf("Error: -p/--passphrase can't be combined with -r/--recipient.") } } @@ -80,6 +85,8 @@ func main() { } defer f.Close() in = f + } else { + stdinInUse = true } if name := outFlag; name != "" && name != "-" { f, err := os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0666) @@ -104,23 +111,45 @@ func main() { } switch { + case passFlag: + fmt.Fprintf(os.Stderr, "Enter passphrase: ") + pass, err := readPassphrase() + if err != nil { + log.Fatalf("Error: could not read passphrase: %v", err) + } + if decryptFlag { + decryptPass(string(pass), in, out) + } else { + encryptPass(string(pass), in, out, armorFlag) + } case decryptFlag: - decrypt(identityFlags, in, out) + decryptKeys(identityFlags, in, out) default: - encrypt(recipientFlags, in, out, armorFlag) + encryptKeys(recipientFlags, in, out, armorFlag) } } -func encrypt(args []string, in io.Reader, out io.Writer, armor bool) { +func encryptKeys(keys []string, in io.Reader, out io.Writer, armor bool) { var recipients []age.Recipient - for _, arg := range args { + for _, arg := range keys { r, err := parseRecipient(arg) if err != nil { log.Fatalf("Error: %v", err) } recipients = append(recipients, r) } + encrypt(recipients, in, out, armor) +} +func encryptPass(pass string, in io.Reader, out io.Writer, armor bool) { + r, err := age.NewScryptRecipient(pass) + if err != nil { + log.Fatalf("Error: %v", err) + } + encrypt([]age.Recipient{r}, in, out, armor) +} + +func encrypt(recipients []age.Recipient, in io.Reader, out io.Writer, armor bool) { ageEncrypt := age.Encrypt if armor { ageEncrypt = age.EncryptWithArmor @@ -137,18 +166,29 @@ func encrypt(args []string, in io.Reader, out io.Writer, armor bool) { } } -func decrypt(args []string, in io.Reader, out io.Writer) { +func decryptKeys(keys []string, in io.Reader, out io.Writer) { var identities []age.Identity // TODO: use the default location if no arguments are provided: // os.UserConfigDir()/age/keys.txt, ~/.ssh/id_rsa, ~/.ssh/id_ed25519 - for _, name := range args { + for _, name := range keys { ids, err := parseIdentitiesFile(name) if err != nil { log.Fatalf("Error: %v", err) } identities = append(identities, ids...) } + decrypt(identities, in, out) +} +func decryptPass(pass string, in io.Reader, out io.Writer) { + i, err := age.NewScryptIdentity(pass) + if err != nil { + log.Fatalf("Error: %v", err) + } + decrypt([]age.Identity{i}, in, out) +} + +func decrypt(identities []age.Identity, in io.Reader, out io.Writer) { r, err := age.Decrypt(in, identities...) if err != nil { log.Fatalf("Error: %v", err) diff --git a/cmd/age/encrypted_keys.go b/cmd/age/encrypted_keys.go index 0a08de8..74642e4 100644 --- a/cmd/age/encrypted_keys.go +++ b/cmd/age/encrypted_keys.go @@ -91,23 +91,23 @@ func (i *EncryptedSSHIdentity) Matches(block *format.Recipient) error { return nil } -func passphrasePrompt(name string) func() ([]byte, error) { - return func() ([]byte, error) { - fd := int(os.Stdin.Fd()) - if !terminal.IsTerminal(fd) { - tty, err := os.Open("/dev/tty") - if err != nil { - return nil, fmt.Errorf("could not read passphrase for %q: standard input is not a terminal, and opening /dev/tty failed: %v", name, err) - } - defer tty.Close() - fd = int(tty.Fd()) - } - fmt.Fprintf(os.Stderr, "Enter passphrase for %q: ", name) - defer fmt.Fprintf(os.Stderr, "\n") - p, err := terminal.ReadPassword(fd) +// stdinInUse is set in main. It's a singleton like os.Stdin. +var stdinInUse bool + +func readPassphrase() ([]byte, error) { + fd := int(os.Stdin.Fd()) + if !terminal.IsTerminal(fd) || stdinInUse { + tty, err := os.Open("/dev/tty") if err != nil { - return nil, fmt.Errorf("could not read passphrase for %q: %v", name, err) + return nil, fmt.Errorf("standard input is not available or not a terminal, and opening /dev/tty failed: %v", err) } - return p, nil + defer tty.Close() + fd = int(tty.Fd()) } + defer fmt.Fprintf(os.Stderr, "\n") + p, err := terminal.ReadPassword(fd) + if err != nil { + return nil, err + } + return p, nil } diff --git a/cmd/age/parse.go b/cmd/age/parse.go index f028c7c..27031be 100644 --- a/cmd/age/parse.go +++ b/cmd/age/parse.go @@ -91,7 +91,15 @@ func parseSSHIdentity(name string, pemBytes []byte) ([]age.Identity, error) { return nil, err } } - i, err := NewEncryptedSSHIdentity(pubKey, pemBytes, passphrasePrompt(name)) + passphrasePrompt := func() ([]byte, error) { + fmt.Fprintf(os.Stderr, "Enter passphrase for %q: ", name) + pass, err := readPassphrase() + if err != nil { + return nil, fmt.Errorf("could not read passphrase for %q: %v", name, err) + } + return pass, nil + } + i, err := NewEncryptedSSHIdentity(pubKey, pemBytes, passphrasePrompt) if err != nil { return nil, err }