Skip to content

Commit

Permalink
feat: move tui code to tui package
Browse files Browse the repository at this point in the history
Makes PluginTerminalUI available for sops (and other third parties) so they can more easily support plugins.
  • Loading branch information
brianmcgee committed Oct 2, 2024
1 parent 176e245 commit 83dce0c
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 85 deletions.
85 changes: 43 additions & 42 deletions cmd/age/age.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package main
import (
"bufio"
"bytes"
"filippo.io/age/tui"
"flag"
"fmt"
"io"
Expand Down Expand Up @@ -104,7 +105,7 @@ func main() {

if len(os.Args) == 1 {
flag.Usage()
exit(1)
tui.Exit(1)
}

var (
Expand Down Expand Up @@ -180,47 +181,47 @@ func main() {
hints = append(hints, "only a single input file may be specified at a time")
}

errorWithHint("too many INPUT arguments: "+quotedArgs, hints...)
tui.ErrorWithHint("too many INPUT arguments: "+quotedArgs, hints...)
}

switch {
case decryptFlag:
if encryptFlag {
errorf("-e/--encrypt can't be used with -d/--decrypt")
tui.Errorf("-e/--encrypt can't be used with -d/--decrypt")
}
if armorFlag {
errorWithHint("-a/--armor can't be used with -d/--decrypt",
tui.ErrorWithHint("-a/--armor can't be used with -d/--decrypt",
"note that armored files are detected automatically")
}
if passFlag {
errorWithHint("-p/--passphrase can't be used with -d/--decrypt",
tui.ErrorWithHint("-p/--passphrase can't be used with -d/--decrypt",
"note that password protected files are detected automatically")
}
if len(recipientFlags) > 0 {
errorWithHint("-r/--recipient can't be used with -d/--decrypt",
tui.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 {
errorWithHint("-R/--recipients-file can't be used with -d/--decrypt",
tui.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 {
errorWithHint("-i/--identity and -j can't be used in encryption mode unless symmetric encryption is explicitly selected with -e/--encrypt",
tui.ErrorWithHint("-i/--identity and -j 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 {
errorWithHint("missing recipients",
tui.ErrorWithHint("missing recipients",
"did you forget to specify -r/--recipient, -R/--recipients-file or -p/--passphrase?")
}
if len(recipientFlags) > 0 && passFlag {
errorf("-p/--passphrase can't be combined with -r/--recipient")
tui.Errorf("-p/--passphrase can't be combined with -r/--recipient")
}
if len(recipientsFileFlags) > 0 && passFlag {
errorf("-p/--passphrase can't be combined with -R/--recipients-file")
tui.Errorf("-p/--passphrase can't be combined with -R/--recipients-file")
}
if len(identityFlags) > 0 && passFlag {
errorf("-p/--passphrase can't be combined with -i/--identity and -j")
tui.Errorf("-p/--passphrase can't be combined with -i/--identity and -j")
}
}

Expand All @@ -241,7 +242,7 @@ func main() {
inUseFiles = append(inUseFiles, absPath(name))
f, err := os.Open(name)
if err != nil {
errorf("failed to open input file %q: %v", name, err)
tui.Errorf("failed to open input file %q: %v", name, err)
}
defer f.Close()
in = f
Expand All @@ -251,23 +252,23 @@ func main() {
// If the input comes from a TTY, assume it's armored, and buffer up
// to the END line (or EOF/EOT) so that a password prompt or the
// output don't get in the way of typing the input. See Issue 364.
buf, err := bufferTerminalInput(in)
buf, err := tui.BufferTerminalInput(in)
if err != nil {
errorf("failed to buffer terminal input: %v", err)
tui.Errorf("failed to buffer terminal input: %v", err)
}
in = buf
}
}
if name := outFlag; name != "" && name != "-" {
for _, f := range inUseFiles {
if f == absPath(name) {
errorf("input and output file are the same: %q", name)
tui.Errorf("input and output file are the same: %q", name)
}
}
f := newLazyOpener(name)
defer func() {
if err := f.Close(); err != nil {
errorf("failed to close output file %q: %v", name, err)
tui.Errorf("failed to close output file %q: %v", name, err)
}
}()
out = f
Expand All @@ -278,7 +279,7 @@ func main() {
} else if !armorFlag {
// If the output wouldn't be armored, refuse to send binary to
// the terminal unless explicitly requested with "-o -".
errorWithHint("refusing to output binary to the terminal",
tui.ErrorWithHint("refusing to output binary to the terminal",
"did you mean to use -a/--armor?",
`force anyway with "-o -"`)
}
Expand All @@ -305,7 +306,7 @@ func main() {
}

func passphrasePromptForEncryption() (string, error) {
pass, err := readSecret("Enter passphrase (leave empty to autogenerate a secure one):")
pass, err := tui.ReadSecret("Enter passphrase (leave empty to autogenerate a secure one):")
if err != nil {
return "", fmt.Errorf("could not read passphrase: %v", err)
}
Expand All @@ -316,12 +317,12 @@ func passphrasePromptForEncryption() (string, error) {
words = append(words, randomWord())
}
p = strings.Join(words, "-")
err := printfToTerminal("using autogenerated passphrase %q", p)
err := tui.PrintfToTerminal("using autogenerated passphrase %q", p)
if err != nil {
return "", fmt.Errorf("could not print passphrase: %v", err)
}
} else {
confirm, err := readSecret("Confirm passphrase:")
confirm, err := tui.ReadSecret("Confirm passphrase:")
if err != nil {
return "", fmt.Errorf("could not read passphrase: %v", err)
}
Expand All @@ -337,19 +338,19 @@ func encryptNotPass(recs, files []string, identities identityFlags, in io.Reader
for _, arg := range recs {
r, err := parseRecipient(arg)
if err, ok := err.(gitHubRecipientError); ok {
errorWithHint(err.Error(), "instead, use recipient files like",
tui.ErrorWithHint(err.Error(), "instead, use recipient files like",
" curl -O https://github.com/"+err.username+".keys",
" age -R "+err.username+".keys")
}
if err != nil {
errorf("%v", err)
tui.Errorf("%v", err)
}
recipients = append(recipients, r)
}
for _, name := range files {
recs, err := parseRecipientsFile(name)
if err != nil {
errorf("failed to parse recipient file %q: %v", name, err)
tui.Errorf("failed to parse recipient file %q: %v", name, err)
}
recipients = append(recipients, recs...)
}
Expand All @@ -358,17 +359,17 @@ func encryptNotPass(recs, files []string, identities identityFlags, in io.Reader
case "i":
ids, err := parseIdentitiesFile(f.Value)
if err != nil {
errorf("reading %q: %v", f.Value, err)
tui.Errorf("reading %q: %v", f.Value, err)
}
r, err := identitiesToRecipients(ids)
if err != nil {
errorf("internal error processing %q: %v", f.Value, err)
tui.Errorf("internal error processing %q: %v", f.Value, err)
}
recipients = append(recipients, r...)
case "j":
id, err := plugin.NewIdentityWithoutData(f.Value, pluginTerminalUI)
id, err := plugin.NewIdentityWithoutData(f.Value, tui.PluginTerminalUI)
if err != nil {
errorf("initializing %q: %v", f.Value, err)
tui.Errorf("initializing %q: %v", f.Value, err)
}
recipients = append(recipients, id.Recipient())
}
Expand All @@ -379,12 +380,12 @@ func encryptNotPass(recs, files []string, identities identityFlags, in io.Reader
func encryptPass(in io.Reader, out io.Writer, armor bool) {
pass, err := passphrasePromptForEncryption()
if err != nil {
errorf("%v", err)
tui.Errorf("%v", err)
}

r, err := age.NewScryptRecipient(pass)
if err != nil {
errorf("%v", err)
tui.Errorf("%v", err)
}
testOnlyConfigureScryptIdentity(r)
encrypt([]age.Recipient{r}, in, out, armor)
Expand All @@ -397,20 +398,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 {
errorf("%v", err)
tui.Errorf("%v", err)
}
}()
out = a
}
w, err := age.Encrypt(out, recipients...)
if err != nil {
errorf("%v", err)
tui.Errorf("%v", err)
}
if _, err := io.Copy(w, in); err != nil {
errorf("%v", err)
tui.Errorf("%v", err)
}
if err := w.Close(); err != nil {
errorf("%v", err)
tui.Errorf("%v", err)
}
}

Expand All @@ -426,7 +427,7 @@ func (rejectScryptIdentity) Unwrap(stanzas []*age.Stanza) ([]byte, error) {
if len(stanzas) != 1 || stanzas[0].Type != "scrypt" {
return nil, age.ErrIncorrectIdentity
}
errorWithHint("file is passphrase-encrypted but identities were specified with -i/--identity or -j",
tui.ErrorWithHint("file is passphrase-encrypted but identities were specified with -i/--identity or -j",
"remove all -i/--identity/-j flags to decrypt passphrase-encrypted files")
panic("unreachable")
}
Expand All @@ -439,13 +440,13 @@ func decryptNotPass(flags identityFlags, in io.Reader, out io.Writer) {
case "i":
ids, err := parseIdentitiesFile(f.Value)
if err != nil {
errorf("reading %q: %v", f.Value, err)
tui.Errorf("reading %q: %v", f.Value, err)
}
identities = append(identities, ids...)
case "j":
id, err := plugin.NewIdentityWithoutData(f.Value, pluginTerminalUI)
id, err := plugin.NewIdentityWithoutData(f.Value, tui.PluginTerminalUI)
if err != nil {
errorf("initializing %q: %v", f.Value, err)
tui.Errorf("initializing %q: %v", f.Value, err)
}
identities = append(identities, id)
}
Expand All @@ -468,7 +469,7 @@ func decrypt(identities []age.Identity, in io.Reader, out io.Writer) {
rr := bufio.NewReader(in)
if intro, _ := rr.Peek(len(crlfMangledIntro)); string(intro) == crlfMangledIntro ||
string(intro) == utf16MangledIntro {
errorWithHint("invalid header intro",
tui.ErrorWithHint("invalid header intro",
"it looks like this file was corrupted by PowerShell redirection",
"consider using -o or -a to encrypt files in PowerShell")
}
Expand All @@ -481,16 +482,16 @@ func decrypt(identities []age.Identity, in io.Reader, out io.Writer) {

r, err := age.Decrypt(in, identities...)
if err != nil {
errorf("%v", err)
tui.Errorf("%v", err)
}
out.Write(nil) // trigger the lazyOpener even if r is empty
if _, err := io.Copy(out, r); err != nil {
errorf("%v", err)
tui.Errorf("%v", err)
}
}

func passphrasePromptForDecryption() (string, error) {
pass, err := readSecret("Enter passphrase:")
pass, err := tui.ReadSecret("Enter passphrase:")
if err != nil {
return "", fmt.Errorf("could not read passphrase: %v", err)
}
Expand Down
5 changes: 3 additions & 2 deletions cmd/age/age_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package main

import (
"bufio"
"filippo.io/age/tui"
"os"
"testing"

Expand All @@ -16,9 +17,9 @@ import (
func TestMain(m *testing.M) {
os.Exit(testscript.RunMain(m, map[string]func() int{
"age": func() (exitCode int) {
testOnlyPanicInsteadOfExit = true
tui.TestOnlyPanicInsteadOfExit = true
defer func() {
if testOnlyDidExit {
if tui.TestOnlyDidExit {
exitCode = recover().(int)
}
}()
Expand Down
13 changes: 7 additions & 6 deletions cmd/age/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package main
import (
"bufio"
"encoding/base64"
"filippo.io/age/tui"
"fmt"
"io"
"os"
Expand All @@ -31,7 +32,7 @@ func (gitHubRecipientError) Error() string {
func parseRecipient(arg string) (age.Recipient, error) {
switch {
case strings.HasPrefix(arg, "age1") && strings.Count(arg, "1") > 1:
return plugin.NewRecipient(arg, pluginTerminalUI)
return plugin.NewRecipient(arg, tui.PluginTerminalUI)
case strings.HasPrefix(arg, "age1"):
return age.ParseX25519Recipient(arg)
case strings.HasPrefix(arg, "ssh-"):
Expand Down Expand Up @@ -79,7 +80,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.
warningf("recipients file %q: ignoring unsupported SSH key of type %q at line %d", name, t, n)
tui.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
Expand Down Expand Up @@ -162,14 +163,14 @@ func parseIdentitiesFile(name string) ([]age.Identity, error) {
return []age.Identity{&EncryptedIdentity{
Contents: contents,
Passphrase: func() (string, error) {
pass, err := readSecret(fmt.Sprintf("Enter passphrase for identity file %q:", name))
pass, err := tui.ReadSecret(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)
tui.Warningf("encrypted identity file %q didn't match file's recipients", name)
},
}}, nil

Expand Down Expand Up @@ -198,7 +199,7 @@ func parseIdentitiesFile(name string) ([]age.Identity, error) {
func parseIdentity(s string) (age.Identity, error) {
switch {
case strings.HasPrefix(s, "AGE-PLUGIN-"):
return plugin.NewIdentity(s, pluginTerminalUI)
return plugin.NewIdentity(s, tui.PluginTerminalUI)
case strings.HasPrefix(s, "AGE-SECRET-KEY-1"):
return age.ParseX25519Identity(s)
default:
Expand Down Expand Up @@ -246,7 +247,7 @@ func parseSSHIdentity(name string, pemBytes []byte) ([]age.Identity, error) {
}
}
passphrasePrompt := func() ([]byte, error) {
pass, err := readSecret(fmt.Sprintf("Enter passphrase for %q:", name))
pass, err := tui.ReadSecret(fmt.Sprintf("Enter passphrase for %q:", name))
if err != nil {
return nil, fmt.Errorf("could not read passphrase for %q: %v", name, err)
}
Expand Down
Loading

0 comments on commit 83dce0c

Please sign in to comment.