diff --git a/cmd/age/parse.go b/cmd/age/parse.go index 16bb15e..bfdc341 100644 --- a/cmd/age/parse.go +++ b/cmd/age/parse.go @@ -37,6 +37,7 @@ func parseRecipient(arg string) (age.Recipient, error) { } r.DisplayMessage = pluginDisplayMessage(r.Name()) r.RequestValue = pluginRequestSecret(r.Name()) + r.Confirm = pluginConfirm(r.Name()) return r, nil case strings.HasPrefix(arg, "age1"): return age.ParseX25519Recipient(arg) @@ -210,6 +211,7 @@ func parseIdentity(s string) (age.Identity, error) { } i.DisplayMessage = pluginDisplayMessage(i.Name()) i.RequestValue = pluginRequestSecret(i.Name()) + i.Confirm = pluginConfirm(i.Name()) return i, nil case strings.HasPrefix(s, "AGE-SECRET-KEY-1"): return age.ParseX25519Identity(s) diff --git a/cmd/age/tui.go b/cmd/age/tui.go index 956f4f8..c6d5855 100644 --- a/cmd/age/tui.go +++ b/cmd/age/tui.go @@ -18,6 +18,7 @@ import ( "log" "os" "runtime" + "strings" "golang.org/x/term" ) @@ -111,3 +112,30 @@ func pluginRequestSecret(name string) func(string, bool) (string, error) { return string(secret), nil } } + +func pluginConfirm(name string) func(msg, yes, no string) (bool, error) { + return func(message, yes, no string) (bool, error) { + if no != "" { + message += fmt.Sprintf(" (1 for %q, 2 for %q)", yes, no) + selection, err := readSecret(message) + if err != nil { + return false, fmt.Errorf("could not read value for age-plugin-%s: %v", name, err) + } + switch strings.TrimSpace(string(selection)) { + case "1": + return true, nil + case "2": + return false, nil + default: + return false, fmt.Errorf("invalid selection %q", selection) + } + } else { + message += fmt.Sprintf(" (press enter for %q)", yes) + _, err := readSecret(message) + if err != nil { + return false, fmt.Errorf("could not read value for age-plugin-%s: %v", name, err) + } + return true, nil + } + } +} diff --git a/internal/plugin/client.go b/internal/plugin/client.go index 15834fe..d7ae5b2 100644 --- a/internal/plugin/client.go +++ b/internal/plugin/client.go @@ -38,6 +38,10 @@ type Recipient struct { // wishes to request a value from the user. If RequestValue is nil or // returns an error, failure will be reported to the plugin. RequestValue func(message string, secret bool) (string, error) + // Confirm is a callback that will be invoked by Unwrap if the plugin wishes + // to request a confirmation from the user. If Confirm is nil or returns an + // error, failure will be reported to the plugin. + Confirm func(message, yes, no string) (bool, error) } var _ age.Recipient = &Recipient{} @@ -152,6 +156,45 @@ ReadLoop: return nil, err } } + case "confirm": + if len(s.Args) != 1 && len(s.Args) != 2 { + return nil, fmt.Errorf("received malformed confirm stanza") + } + if r.Confirm == nil { + ss := &format.Stanza{Type: "fail"} + if err := ss.Marshal(conn); err != nil { + return nil, err + } + break + } + yes, err := format.DecodeString(s.Args[0]) + if err != nil { + return nil, fmt.Errorf("received malformed confirm stanza") + } + var no []byte + if len(s.Args) == 2 { + no, err = format.DecodeString(s.Args[1]) + if err != nil { + return nil, fmt.Errorf("received malformed confirm stanza") + } + } + msg := string(s.Body) + if selection, err := r.Confirm(msg, string(yes), string(no)); err != nil { + ss := &format.Stanza{Type: "fail"} + if err := ss.Marshal(conn); err != nil { + return nil, err + } + } else { + ss := &format.Stanza{Type: "ok"} + if selection { + ss.Args = append(ss.Args, "yes") + } else { + ss.Args = append(ss.Args, "no") + } + if err := ss.Marshal(conn); err != nil { + return nil, err + } + } case "recipient-stanza": if len(s.Args) < 2 { return nil, fmt.Errorf("received malformed recipient stanza") @@ -211,6 +254,10 @@ type Identity struct { // wishes to request a value from the user. If RequestValue is nil or // returns an error, failure will be reported to the plugin. RequestValue func(message string, secret bool) (string, error) + // Confirm is a callback that will be invoked by Unwrap if the plugin wishes + // to request a confirmation from the user. If Confirm is nil or returns an + // error, failure will be reported to the plugin. + Confirm func(message, yes, no string) (bool, error) } var _ age.Identity = &Identity{} @@ -248,6 +295,7 @@ func (i *Identity) Recipient() *Recipient { DisplayMessage: i.DisplayMessage, RequestValue: i.RequestValue, + Confirm: i.Confirm, } } @@ -340,6 +388,45 @@ ReadLoop: return nil, err } } + case "confirm": + if len(s.Args) != 1 && len(s.Args) != 2 { + return nil, fmt.Errorf("received malformed confirm stanza") + } + if i.Confirm == nil { + ss := &format.Stanza{Type: "fail"} + if err := ss.Marshal(conn); err != nil { + return nil, err + } + break + } + yes, err := format.DecodeString(s.Args[0]) + if err != nil { + return nil, fmt.Errorf("received malformed confirm stanza") + } + var no []byte + if len(s.Args) == 2 { + no, err = format.DecodeString(s.Args[1]) + if err != nil { + return nil, fmt.Errorf("received malformed confirm stanza") + } + } + msg := string(s.Body) + if selection, err := i.Confirm(msg, string(yes), string(no)); err != nil { + ss := &format.Stanza{Type: "fail"} + if err := ss.Marshal(conn); err != nil { + return nil, err + } + } else { + ss := &format.Stanza{Type: "ok"} + if selection { + ss.Args = append(ss.Args, "yes") + } else { + ss.Args = append(ss.Args, "no") + } + if err := ss.Marshal(conn); err != nil { + return nil, err + } + } case "file-key": if len(s.Args) != 1 { return nil, fmt.Errorf("received malformed file-key stanza")