Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions age/encrypted_keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ func (i *LazyScryptIdentity) Unwrap(stanzas []*age.Stanza) (fileKey []byte, err
return fileKey, err
}

func unwrapIdentities(key string, reader io.Reader) (ParsedIdentities, error){
func unwrapIdentities(key string, reader io.Reader) (ParsedIdentities, error) {
b := bufio.NewReader(reader)
p, _ := b.Peek(14) // length of "age-encryption" and "-----BEGIN AGE"
peeked := string(p)
Expand Down Expand Up @@ -181,10 +181,10 @@ func unwrapIdentities(key string, reader io.Reader) (ParsedIdentities, error){
return ids, nil
// An unencrypted age identity file.
default:
ids, err := age.ParseIdentities(b)
ids, err := parseIdentities(b)
if err != nil {
return nil, fmt.Errorf("failed to parse '%s' age identities: %w", key, err)
}
return ids, nil
}
}
}
51 changes: 42 additions & 9 deletions age/keysource.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package age

import (
"bufio"
"bytes"
"errors"
"filippo.io/age/plugin"
"fmt"
"io"
"os"
Expand Down Expand Up @@ -115,7 +117,10 @@ type ParsedIdentities []age.Identity
// parsing (using age.ParseIdentities) and appending to the slice yourself, in
// combination with e.g. a sync.Mutex.
func (i *ParsedIdentities) Import(identity ...string) error {
identities, err := parseIdentities(identity...)
// one identity per line
r := strings.NewReader(strings.Join(identity, "\n"))

identities, err := parseIdentities(r)
if err != nil {
return fmt.Errorf("failed to parse and add to age identities: %w", err)
}
Expand Down Expand Up @@ -339,6 +344,12 @@ func (key *MasterKey) loadIdentities() (ParsedIdentities, error) {
// key or a public ssh key.
func parseRecipient(recipient string) (age.Recipient, error) {
switch {
case strings.HasPrefix(recipient, "age1") && strings.Count(recipient, "1") > 1:
parsedRecipient, err := plugin.NewRecipient(recipient, pluginTerminalUI)
if err != nil {
return nil, fmt.Errorf("failed to parse input as age key from age plugin: %w", err)
}
return parsedRecipient, nil
case strings.HasPrefix(recipient, "age1"):
parsedRecipient, err := age.ParseX25519Recipient(recipient)
if err != nil {
Expand All @@ -357,17 +368,39 @@ func parseRecipient(recipient string) (age.Recipient, error) {
return nil, fmt.Errorf("failed to parse input, unknown recipient type: %q", recipient)
}

// parseIdentities attempts to parse the string set of encoded age identities.
// A single identity argument is allowed to be a multiline string containing
// multiple identities. Empty lines and lines starting with "#" are ignored.
func parseIdentities(identity ...string) (ParsedIdentities, error) {
var identities []age.Identity
for _, i := range identity {
parsed, err := age.ParseIdentities(strings.NewReader(i))
// parseIdentities attempts to parse one or more age identities from the provided reader.
// One identity per line.
// Empty lines and lines starting with "#" are ignored.
func parseIdentities(r io.Reader) (ParsedIdentities, error) {
var identities ParsedIdentities

scanner := bufio.NewScanner(r)

for scanner.Scan() {
line := scanner.Text()

if line == "" || strings.HasPrefix(line, "#") {
continue
}

parsed, err := parseIdentity(line)
if err != nil {
return nil, err
}
identities = append(identities, parsed...)

identities = append(identities, parsed)
}

return identities, nil
}

func parseIdentity(s string) (age.Identity, error) {
switch {
case strings.HasPrefix(s, "AGE-PLUGIN-"):
return plugin.NewIdentity(s, pluginTerminalUI)
case strings.HasPrefix(s, "AGE-SECRET-KEY-1"):
return age.ParseX25519Identity(s)
default:
return nil, fmt.Errorf("unknown identity type")
}
}
198 changes: 175 additions & 23 deletions age/tui.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// These functions have been copied from the age project
// https://github.com/FiloSottile/age/blob/v1.0.0/cmd/age/encrypted_keys.go
// https://github.com/FiloSottile/age/blob/3d91014ea095e8d70f7c6c4833f89b53a96e0832/cmd/age/tui.go
//
// Copyright 2021 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
Expand All @@ -11,7 +12,10 @@
package age

import (
"errors"
"filippo.io/age/plugin"
"fmt"
"io"
"os"
"runtime"
"testing"
Expand All @@ -33,37 +37,185 @@ func readPassphrase(prompt string) ([]byte, error) {
}
}

var in, out *os.File
var (
err error
passphrase []byte
)

err = withTerminal(func(in, out *os.File) error {
_, err := fmt.Fprintf(out, "%s ", prompt)
if err != nil {
return fmt.Errorf("could not write prompt: %v", err)
}

// Use CRLF to work around an apparent bug in WSL2's handling of CONOUT$.
// Only when running a Windows binary from WSL2, the cursor would not go
// back to the start of the line with a simple LF. Honestly, it's impressive
// CONIN$ and CONOUT$ even work at all inside WSL2.
defer fmt.Fprintf(out, "\r\n")

if passphrase, err = term.ReadPassword(int(in.Fd())); err != nil {
return fmt.Errorf("could not read passphrase: %v", err)
}

return nil
})

return passphrase, err
}

func printf(format string, v ...interface{}) {
log.Printf("age: "+format, v...)
}

func warningf(format string, v ...interface{}) {
log.Printf("age: warning: "+format, v...)
}

// If testOnlyPanicInsteadOfExit is true, exit will set testOnlyDidExit and
// panic instead of calling os.Exit. This way, the wrapper in TestMain can
// recover the panic and return the exit code only if it was originated in exit.
var testOnlyPanicInsteadOfExit bool
var testOnlyDidExit bool

func exit(code int) {
if testOnlyPanicInsteadOfExit {
testOnlyDidExit = true
panic(code)
}
os.Exit(code)
}

// clearLine clears the current line on the terminal, or opens a new line if
// terminal escape codes don't work.
func clearLine(out io.Writer) {
const (
CUI = "\033[" // Control Sequence Introducer
CPL = CUI + "F" // Cursor Previous Line
EL = CUI + "K" // Erase in Line
)

// First, open a new line, which is guaranteed to work everywhere. Then, try
// to erase the line above with escape codes.
//
// (We use CRLF instead of LF to work around an apparent bug in WSL2's
// handling of CONOUT$. Only when running a Windows binary from WSL2, the
// cursor would not go back to the start of the line with a simple LF.
// Honestly, it's impressive CONIN$ and CONOUT$ work at all inside WSL2.)
fmt.Fprintf(out, "\r\n"+CPL+EL)
}

// withTerminal runs f with the terminal input and output files, if available.
// withTerminal does not open a non-terminal stdin, so the caller does not need
// to check stdinInUse.
func withTerminal(f func(in, out *os.File) error) error {
if runtime.GOOS == "windows" {
var err error
in, err = os.OpenFile("CONIN$", os.O_RDWR, 0)
in, err := os.OpenFile("CONIN$", os.O_RDWR, 0)
if err != nil {
return nil, err
return err
}
defer in.Close()
out, err = os.OpenFile("CONOUT$", os.O_WRONLY, 0)
out, err := os.OpenFile("CONOUT$", os.O_WRONLY, 0)
if err != nil {
return nil, err
return err
}
defer out.Close()
} else if _, err := os.Stat("/dev/tty"); err == nil {
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
if err != nil {
return nil, err
}
return f(in, out)
} else if tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0); err == nil {
defer tty.Close()
in, out = tty, tty
return f(tty, tty)
} else if term.IsTerminal(int(os.Stdin.Fd())) {
return f(os.Stdin, os.Stdin)
} else {
if !term.IsTerminal(int(os.Stdin.Fd())) {
return nil, fmt.Errorf("standard input is not a terminal, and /dev/tty is not available: %v", err)
}
in, out = os.Stdin, os.Stderr
return fmt.Errorf("standard input is not a terminal, and /dev/tty is not available: %v", err)
}
fmt.Fprintf(out, "%s ", prompt)
// Use CRLF to work around an apparent bug in WSL2's handling of CONOUT$.
// Only when running a Windows binary from WSL2, the cursor would not go
// back to the start of the line with a simple LF. Honestly, it's impressive
// CONIN$ and CONOUT$ even work at all inside WSL2.
defer fmt.Fprintf(out, "\r\n")
return term.ReadPassword(int(in.Fd()))
}

// readSecret reads a value from the terminal with no echo. The prompt is ephemeral.
func readSecret(prompt string) (s []byte, err error) {
err = withTerminal(func(in, out *os.File) error {
fmt.Fprintf(out, "%s ", prompt)
defer clearLine(out)
s, err = term.ReadPassword(int(in.Fd()))
return err
})
return
}

// readCharacter reads a single character from the terminal with no echo. The
// prompt is ephemeral.
func readCharacter(prompt string) (c byte, err error) {
err = withTerminal(func(in, out *os.File) error {
fmt.Fprintf(out, "%s ", prompt)
defer clearLine(out)

oldState, err := term.MakeRaw(int(in.Fd()))
if err != nil {
return err
}
defer term.Restore(int(in.Fd()), oldState)

b := make([]byte, 1)
if _, err := in.Read(b); err != nil {
return err
}

c = b[0]
return nil
})
return
}

var pluginTerminalUI = &plugin.ClientUI{
DisplayMessage: func(name, message string) error {
printf("%s plugin: %s", name, message)
return nil
},
RequestValue: func(name, message string, _ bool) (s string, err error) {
defer func() {
if err != nil {
warningf("could not read value for age-plugin-%s: %v", name, err)
}
}()
secret, err := readSecret(message)
if err != nil {
return "", err
}
return string(secret), nil
},
Confirm: func(name, message, yes, no string) (choseYes bool, err error) {
defer func() {
if err != nil {
warningf("could not read value for age-plugin-%s: %v", name, err)
}
}()
if no == "" {
message += fmt.Sprintf(" (press enter for %q)", yes)
_, err := readSecret(message)
if err != nil {
return false, err
}
return true, nil
}
message += fmt.Sprintf(" (press [1] for %q or [2] for %q)", yes, no)
for {
selection, err := readCharacter(message)
if err != nil {
return false, err
}
switch selection {
case '1':
return true, nil
case '2':
return false, nil
case '\x03': // CTRL-C
return false, errors.New("user cancelled prompt")
default:
warningf("reading value for age-plugin-%s: invalid selection %q", name, selection)
}
}
},
WaitTimer: func(name string) {
printf("waiting on %s plugin...", name)
},
}
Loading