mirror of
https://github.com/FiloSottile/age.git
synced 2025-12-23 05:25:14 +00:00
cmd/age: add -j option for data-less plugins
See str4d/rage#237 and str4d/rage#236
This commit is contained in:
@@ -80,6 +80,24 @@ func (f *multiFlag) Set(value string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type identityFlag struct {
|
||||
Type, Value string
|
||||
}
|
||||
|
||||
// identityFlags tracks -i and -j flags, preserving their relative order, so
|
||||
// that "age -d -j agent -i encrypted-fallback-keys.age" behaves as expected.
|
||||
type identityFlags []identityFlag
|
||||
|
||||
func (f *identityFlags) addIdentityFlag(value string) error {
|
||||
*f = append(*f, identityFlag{Type: "i", Value: value})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *identityFlags) addPluginFlag(value string) error {
|
||||
*f = append(*f, identityFlag{Type: "j", Value: value})
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Usage = func() { fmt.Fprintf(os.Stderr, "%s\n", usage) }
|
||||
|
||||
@@ -92,8 +110,9 @@ func main() {
|
||||
outFlag string
|
||||
decryptFlag, encryptFlag bool
|
||||
passFlag, versionFlag, armorFlag bool
|
||||
recipientFlags, identityFlags multiFlag
|
||||
recipientFlags multiFlag
|
||||
recipientsFileFlags multiFlag
|
||||
identityFlags identityFlags
|
||||
)
|
||||
|
||||
flag.BoolVar(&versionFlag, "version", false, "print the version")
|
||||
@@ -111,8 +130,9 @@ func main() {
|
||||
flag.Var(&recipientFlags, "recipient", "recipient (can be repeated)")
|
||||
flag.Var(&recipientsFileFlags, "R", "recipients file (can be repeated)")
|
||||
flag.Var(&recipientsFileFlags, "recipients-file", "recipients file (can be repeated)")
|
||||
flag.Var(&identityFlags, "i", "identity (can be repeated)")
|
||||
flag.Var(&identityFlags, "identity", "identity (can be repeated)")
|
||||
flag.Func("i", "identity (can be repeated)", identityFlags.addIdentityFlag)
|
||||
flag.Func("identity", "identity (can be repeated)", identityFlags.addIdentityFlag)
|
||||
flag.Func("j", "data-less plugin (can be repeated)", identityFlags.addPluginFlag)
|
||||
flag.Parse()
|
||||
|
||||
if versionFlag {
|
||||
@@ -185,7 +205,7 @@ func main() {
|
||||
}
|
||||
default: // encrypt
|
||||
if len(identityFlags) > 0 && !encryptFlag {
|
||||
errorWithHint("-i/--identity can't be used in encryption mode unless symmetric encryption is explicitly selected with -e/--encrypt",
|
||||
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 {
|
||||
@@ -199,7 +219,7 @@ func main() {
|
||||
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")
|
||||
errorf("-p/--passphrase can't be combined with -i/--identity and -j")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,9 +308,9 @@ func passphrasePromptForEncryption() (string, error) {
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func encryptNotPass(keys, files, identities []string, in io.Reader, out io.Writer, armor bool) {
|
||||
func encryptNotPass(recs, files []string, identities identityFlags, in io.Reader, out io.Writer, armor bool) {
|
||||
var recipients []age.Recipient
|
||||
for _, arg := range keys {
|
||||
for _, arg := range recs {
|
||||
r, err := parseRecipient(arg)
|
||||
if err, ok := err.(gitHubRecipientError); ok {
|
||||
errorWithHint(err.Error(), "instead, use recipient files like",
|
||||
@@ -309,16 +329,25 @@ func encryptNotPass(keys, files, identities []string, in io.Reader, out io.Write
|
||||
}
|
||||
recipients = append(recipients, recs...)
|
||||
}
|
||||
for _, name := range identities {
|
||||
ids, err := parseIdentitiesFile(name)
|
||||
if err != nil {
|
||||
errorf("reading %q: %v", name, err)
|
||||
for _, f := range identities {
|
||||
switch f.Type {
|
||||
case "i":
|
||||
ids, err := parseIdentitiesFile(f.Value)
|
||||
if err != nil {
|
||||
errorf("reading %q: %v", f.Value, err)
|
||||
}
|
||||
r, err := identitiesToRecipients(ids)
|
||||
if err != nil {
|
||||
errorf("internal error processing %q: %v", f.Value, err)
|
||||
}
|
||||
recipients = append(recipients, r...)
|
||||
case "j":
|
||||
id, err := plugin.NewIdentityWithoutData(f.Value, pluginTerminalUI)
|
||||
if err != nil {
|
||||
errorf("initializing %q: %v", f.Value, err)
|
||||
}
|
||||
recipients = append(recipients, id.Recipient())
|
||||
}
|
||||
r, err := identitiesToRecipients(ids)
|
||||
if err != nil {
|
||||
errorf("internal error processing %q: %v", name, err)
|
||||
}
|
||||
recipients = append(recipients, r...)
|
||||
}
|
||||
encrypt(recipients, in, out, armor)
|
||||
}
|
||||
@@ -359,19 +388,28 @@ func encrypt(recipients []age.Recipient, in io.Reader, out io.Writer, withArmor
|
||||
const crlfMangledIntro = "age-encryption.org/v1" + "\r"
|
||||
const utf16MangledIntro = "\xff\xfe" + "a\x00g\x00e\x00-\x00e\x00n\x00c\x00r\x00y\x00p\x00"
|
||||
|
||||
func decrypt(keys []string, in io.Reader, out io.Writer) {
|
||||
func decrypt(flags identityFlags, in io.Reader, out io.Writer) {
|
||||
identities := []age.Identity{
|
||||
// If there is an scrypt recipient (it will have to be the only one and)
|
||||
// this identity will be invoked.
|
||||
&LazyScryptIdentity{passphrasePrompt},
|
||||
&LazyScryptIdentity{passphrasePromptForDecryption},
|
||||
}
|
||||
|
||||
for _, name := range keys {
|
||||
ids, err := parseIdentitiesFile(name)
|
||||
if err != nil {
|
||||
errorf("reading %q: %v", name, err)
|
||||
for _, f := range flags {
|
||||
switch f.Type {
|
||||
case "i":
|
||||
ids, err := parseIdentitiesFile(f.Value)
|
||||
if err != nil {
|
||||
errorf("reading %q: %v", f.Value, err)
|
||||
}
|
||||
identities = append(identities, ids...)
|
||||
case "j":
|
||||
id, err := plugin.NewIdentityWithoutData(f.Value, pluginTerminalUI)
|
||||
if err != nil {
|
||||
errorf("initializing %q: %v", f.Value, err)
|
||||
}
|
||||
identities = append(identities, id)
|
||||
}
|
||||
identities = append(identities, ids...)
|
||||
}
|
||||
|
||||
rr := bufio.NewReader(in)
|
||||
@@ -397,7 +435,7 @@ func decrypt(keys []string, in io.Reader, out io.Writer) {
|
||||
}
|
||||
}
|
||||
|
||||
func passphrasePrompt() (string, error) {
|
||||
func passphrasePromptForDecryption() (string, error) {
|
||||
pass, err := readSecret("Enter passphrase:")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not read passphrase: %v", err)
|
||||
|
||||
@@ -31,14 +31,7 @@ func (gitHubRecipientError) Error() string {
|
||||
func parseRecipient(arg string) (age.Recipient, error) {
|
||||
switch {
|
||||
case strings.HasPrefix(arg, "age1") && strings.Count(arg, "1") > 1:
|
||||
r, err := plugin.NewRecipient(arg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.DisplayMessage = pluginDisplayMessage(r.Name())
|
||||
r.RequestValue = pluginRequestSecret(r.Name())
|
||||
r.Confirm = pluginConfirm(r.Name())
|
||||
return r, nil
|
||||
return plugin.NewRecipient(arg, pluginTerminalUI)
|
||||
case strings.HasPrefix(arg, "age1"):
|
||||
return age.ParseX25519Recipient(arg)
|
||||
case strings.HasPrefix(arg, "ssh-"):
|
||||
@@ -205,14 +198,7 @@ func parseIdentitiesFile(name string) ([]age.Identity, error) {
|
||||
func parseIdentity(s string) (age.Identity, error) {
|
||||
switch {
|
||||
case strings.HasPrefix(s, "AGE-PLUGIN-"):
|
||||
i, err := plugin.NewIdentity(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
i.DisplayMessage = pluginDisplayMessage(i.Name())
|
||||
i.RequestValue = pluginRequestSecret(i.Name())
|
||||
i.Confirm = pluginConfirm(i.Name())
|
||||
return i, nil
|
||||
return plugin.NewIdentity(s, pluginTerminalUI)
|
||||
case strings.HasPrefix(s, "AGE-SECRET-KEY-1"):
|
||||
return age.ParseX25519Identity(s)
|
||||
default:
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"filippo.io/age/internal/plugin"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
@@ -96,25 +97,19 @@ func readSecret(prompt string) ([]byte, error) {
|
||||
return term.ReadPassword(int(in.Fd()))
|
||||
}
|
||||
|
||||
func pluginDisplayMessage(name string) func(string) error {
|
||||
return func(message string) error {
|
||||
var pluginTerminalUI = &plugin.ClientUI{
|
||||
DisplayMessage: func(name, message string) error {
|
||||
printf("%s plugin: %s", name, message)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func pluginRequestSecret(name string) func(string, bool) (string, error) {
|
||||
return func(message string, _ bool) (string, error) {
|
||||
},
|
||||
RequestValue: func(name, message string, _ bool) (string, error) {
|
||||
secret, err := readSecret(message)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not read value for age-plugin-%s: %v", name, err)
|
||||
}
|
||||
return string(secret), nil
|
||||
}
|
||||
}
|
||||
|
||||
func pluginConfirm(name string) func(msg, yes, no string) (bool, error) {
|
||||
return func(message, yes, no string) (bool, error) {
|
||||
},
|
||||
Confirm: func(name, message, yes, no string) (bool, error) {
|
||||
if no != "" {
|
||||
message += fmt.Sprintf(" (1 for %q, 2 for %q)", yes, no)
|
||||
selection, err := readSecret(message)
|
||||
@@ -137,5 +132,5 @@ func pluginConfirm(name string) func(msg, yes, no string) (bool, error) {
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ age(1) -- simple, modern, and secure file encryption
|
||||
|
||||
`age` [`--encrypt`] (`-r` <RECIPIENT> | `-R` <PATH>)... [`--armor`] [`-o` <OUTPUT>] [<INPUT>]<br>
|
||||
`age` [`--encrypt`] `--passphrase` [`--armor`] [`-o` <OUTPUT>] [<INPUT>]<br>
|
||||
`age` `--decrypt` [`-i` <PATH>]... [`-o` <OUTPUT>] [<INPUT>]<br>
|
||||
`age` `--decrypt` [`-i` <PATH> | `-j` <PLUGIN>]... [`-o` <OUTPUT>] [<INPUT>]<br>
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
@@ -120,6 +120,15 @@ overhead per recipient, plus 16 bytes every 64KiB of plaintext.
|
||||
corresponding to the `IDENTITIES` listed at <PATH>. This allows using an
|
||||
identity file as a symmetric key, if desired.
|
||||
|
||||
* `-j` <PLUGIN>:
|
||||
Decrypt using the data-less [plugin][Plugins] <PLUGIN>.
|
||||
|
||||
This is equivalent to using `-i`/`--identity` with a file that contains a
|
||||
single plugin `IDENTITY` that encodes no plugin-specific data.
|
||||
|
||||
If `-e`/`--encrypt` is explicitly specified (to avoid confusion), `-j` may
|
||||
also be used to encrypt with a data-less plugin.
|
||||
|
||||
## RECIPIENTS AND IDENTITIES
|
||||
|
||||
`RECIPIENTS` are public values, like a public key, that a file can be encrypted
|
||||
@@ -194,9 +203,12 @@ the plugin. For example, a plugin can be used to decrypt files encrypted to a
|
||||
native X25519 `RECIPIENT` or even with a passphrase. Similarly, a plugin can
|
||||
encrypt a file such that it can be decrypted without the use of any plugin.
|
||||
|
||||
Plugins for which the `IDENTITY`/`RECIPIENT` distinction doesn't make sense may
|
||||
generate only an `IDENTITY` and instruct the user to perform encryption with the
|
||||
`-e`/`--encrypt` and `-i`/`--identity` flags.
|
||||
Plugins for which the `IDENTITY`/`RECIPIENT` distinction doesn't make sense
|
||||
(such as a symmetric encryption plugin) may generate only an `IDENTITY` and
|
||||
instruct the user to perform encryption with the `-e`/`--encrypt` and
|
||||
`-i`/`--identity` flags. Plugins for which the concept of separate identities
|
||||
doesn't make sense (such as a password-encryption plugin) may instruct the user
|
||||
to use the `-j` flag.
|
||||
|
||||
## EXIT STATUS
|
||||
|
||||
|
||||
@@ -26,16 +26,15 @@ import (
|
||||
type Recipient struct {
|
||||
name string
|
||||
encoding string
|
||||
ui *ClientUI
|
||||
|
||||
// identity is true when encoding is an identity string.
|
||||
identity bool
|
||||
|
||||
ClientUI
|
||||
}
|
||||
|
||||
var _ age.Recipient = &Recipient{}
|
||||
|
||||
func NewRecipient(s string) (*Recipient, error) {
|
||||
func NewRecipient(s string, ui *ClientUI) (*Recipient, error) {
|
||||
hrp, _, err := bech32.Decode(s)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid recipient encoding %q: %v", s, err)
|
||||
@@ -45,7 +44,7 @@ func NewRecipient(s string) (*Recipient, error) {
|
||||
}
|
||||
name := strings.TrimPrefix(hrp, "age1")
|
||||
return &Recipient{
|
||||
name: name, encoding: s,
|
||||
name: name, encoding: s, ui: ui,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -125,7 +124,7 @@ ReadLoop:
|
||||
case "done":
|
||||
break ReadLoop
|
||||
default:
|
||||
if ok, err := r.handleUI(conn, s); err != nil {
|
||||
if ok, err := r.ui.handle(r.name, conn, s); err != nil {
|
||||
return nil, err
|
||||
} else if !ok {
|
||||
if err := writeStanza(conn, "unsupported"); err != nil {
|
||||
@@ -145,13 +144,12 @@ ReadLoop:
|
||||
type Identity struct {
|
||||
name string
|
||||
encoding string
|
||||
|
||||
ClientUI
|
||||
ui *ClientUI
|
||||
}
|
||||
|
||||
var _ age.Identity = &Identity{}
|
||||
|
||||
func NewIdentity(s string) (*Identity, error) {
|
||||
func NewIdentity(s string, ui *ClientUI) (*Identity, error) {
|
||||
hrp, _, err := bech32.Decode(s)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid identity encoding: %v", err)
|
||||
@@ -162,7 +160,17 @@ func NewIdentity(s string) (*Identity, error) {
|
||||
name := strings.TrimSuffix(strings.TrimPrefix(hrp, "AGE-PLUGIN-"), "-")
|
||||
name = strings.ToLower(name)
|
||||
return &Identity{
|
||||
name: name, encoding: s,
|
||||
name: name, encoding: s, ui: ui,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewIdentityWithoutData(name string, ui *ClientUI) (*Identity, error) {
|
||||
s, err := bech32.Encode("AGE-PLUGIN-"+strings.ToUpper(name)+"-", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Identity{
|
||||
name: name, encoding: s, ui: ui,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -181,7 +189,7 @@ func (i *Identity) Recipient() *Recipient {
|
||||
name: i.name,
|
||||
encoding: i.encoding,
|
||||
identity: true,
|
||||
ClientUI: i.ClientUI,
|
||||
ui: i.ui,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,7 +264,7 @@ ReadLoop:
|
||||
case "done":
|
||||
break ReadLoop
|
||||
default:
|
||||
if ok, err := i.handleUI(conn, s); err != nil {
|
||||
if ok, err := i.ui.handle(i.name, conn, s); err != nil {
|
||||
return nil, err
|
||||
} else if !ok {
|
||||
if err := writeStanza(conn, "unsupported"); err != nil {
|
||||
@@ -278,25 +286,25 @@ ReadLoop:
|
||||
type ClientUI struct {
|
||||
// DisplayMessage displays the message, which is expected to have lowercase
|
||||
// initials and no final period.
|
||||
DisplayMessage func(message string) error
|
||||
DisplayMessage func(name, message string) error
|
||||
|
||||
// RequestValue requests a secret or public input, with the provided prompt.
|
||||
RequestValue func(prompt string, secret bool) (string, error)
|
||||
RequestValue func(name, prompt string, secret bool) (string, error)
|
||||
|
||||
// Confirm requests a confirmation with the provided prompt. The yes and no
|
||||
// value are the choices provided to the user. no may be empty. The return
|
||||
// value indicates whether the user selected the yes or no option.
|
||||
Confirm func(prompt, yes, no string) (choseYes bool, err error)
|
||||
Confirm func(name, prompt, yes, no string) (choseYes bool, err error)
|
||||
}
|
||||
|
||||
func (c *ClientUI) handleUI(conn *clientConnection, s *format.Stanza) (ok bool, err error) {
|
||||
func (c *ClientUI) handle(name string, conn *clientConnection, s *format.Stanza) (ok bool, err error) {
|
||||
// TODO: surface non-fatal but probably useful errors.
|
||||
switch s.Type {
|
||||
case "msg":
|
||||
if c.DisplayMessage == nil {
|
||||
return true, writeStanza(conn, "fail")
|
||||
}
|
||||
if err := c.DisplayMessage(string(s.Body)); err != nil {
|
||||
if err := c.DisplayMessage(name, string(s.Body)); err != nil {
|
||||
return true, writeStanza(conn, "fail")
|
||||
}
|
||||
return true, writeStanza(conn, "ok")
|
||||
@@ -304,7 +312,7 @@ func (c *ClientUI) handleUI(conn *clientConnection, s *format.Stanza) (ok bool,
|
||||
if c.RequestValue == nil {
|
||||
return true, writeStanza(conn, "fail")
|
||||
}
|
||||
secret, err := c.RequestValue(string(s.Body), s.Type == "request-secret")
|
||||
secret, err := c.RequestValue(name, string(s.Body), s.Type == "request-secret")
|
||||
if err != nil {
|
||||
return true, writeStanza(conn, "fail")
|
||||
}
|
||||
@@ -327,7 +335,7 @@ func (c *ClientUI) handleUI(conn *clientConnection, s *format.Stanza) (ok bool,
|
||||
return true, fmt.Errorf("malformed confirm stanza: invalid NO option encoding")
|
||||
}
|
||||
}
|
||||
choseYes, err := c.Confirm(string(s.Body), string(yes), string(no))
|
||||
choseYes, err := c.Confirm(name, string(s.Body), string(yes), string(no))
|
||||
if err != nil {
|
||||
return true, writeStanza(conn, "fail")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user