diff --git a/README.md b/README.md index 57b39bd..84d3348 100644 --- a/README.md +++ b/README.md @@ -125,8 +125,9 @@ joined by single spaces, whatever whitespace is around or between them, so one word per line, tabs or extra spaces give the same keys. `KEYFUNC_MNEMONIC` and `KEYFUNC_MNEMONIC_COMMAND` are removed from the -environment before the system `ssh` (`keyfunc ssh to`) and `sftp` -(`keyfunc ssh install`) are started, so the mnemonic is never handed on to them. +environment before the mnemonic command, the system `ssh` (`keyfunc ssh to`) and +`sftp` (`keyfunc ssh install`) are started, so the mnemonic is never handed on +to them. Every command takes `--index` / `-n` and `--mnemonic-command`, and has `--help`. `keyfunc --version` prints the version. `make build` stamps it; a binary diff --git a/internal/cli/ssh/install.go b/internal/cli/ssh/install.go index 1837e68..4a217cb 100644 --- a/internal/cli/ssh/install.go +++ b/internal/cli/ssh/install.go @@ -16,6 +16,7 @@ import ( "github.com/spf13/cobra" "sneak.berlin/go/keyfunc/internal/cli/signals" + "sneak.berlin/go/keyfunc/internal/mnemonic" ) // Where the key goes on the host and what the file it arrives in is @@ -220,7 +221,7 @@ func session( //nolint:gosec // the options are the user's own, meant for sftp command := exec.CommandContext(cmd.Context(), "sftp", argv...) - command.Env = childEnv() + command.Env = mnemonic.ChildEnv() command.Stdin = strings.NewReader(strings.Join(batch, "\n") + "\n") command.Stdout = &said command.Stderr = &said diff --git a/internal/cli/ssh/ssh.go b/internal/cli/ssh/ssh.go index 9fa73f6..c772a62 100644 --- a/internal/cli/ssh/ssh.go +++ b/internal/cli/ssh/ssh.go @@ -3,13 +3,10 @@ package ssh import ( "fmt" - "os" - "strings" "github.com/spf13/cobra" "sneak.berlin/go/keyfunc/internal/cli/options" "sneak.berlin/go/keyfunc/internal/derive" - "sneak.berlin/go/keyfunc/internal/mnemonic" "sneak.berlin/go/keyfunc/internal/sshkey" ) @@ -87,26 +84,6 @@ func write(cmd *cobra.Command, text string) error { return nil } -// childEnv is the tool's environment with the mnemonic variables taken -// out, for the ssh and sftp children it starts. "ssh to" exists so the -// private key never leaves the tool; the mnemonic, from either variable, -// must not leave it either. -func childEnv() []string { - environ := os.Environ() - kept := make([]string, 0, len(environ)) - - for _, entry := range environ { - name, _, _ := strings.Cut(entry, "=") - if name == mnemonic.Variable || name == mnemonic.CommandVariable { - continue - } - - kept = append(kept, entry) - } - - return kept -} - // addComment gives a command its comment flag. func addComment(cmd *cobra.Command) { cmd.Flags().String( diff --git a/internal/cli/ssh/to.go b/internal/cli/ssh/to.go index c165796..9586800 100644 --- a/internal/cli/ssh/to.go +++ b/internal/cli/ssh/to.go @@ -11,6 +11,7 @@ import ( "github.com/spf13/cobra" "sneak.berlin/go/keyfunc/internal/cli/signals" + "sneak.berlin/go/keyfunc/internal/mnemonic" ) // StatusError says the tool should end with the status ssh ended with. @@ -79,7 +80,7 @@ func to() *cobra.Command { func connect(ctx context.Context, argv []string) error { //nolint:gosec // the arguments are the user's own, meant for ssh command := exec.CommandContext(ctx, "ssh", argv...) - command.Env = childEnv() + command.Env = mnemonic.ChildEnv() command.Stdin = os.Stdin command.Stdout = os.Stdout command.Stderr = os.Stderr diff --git a/internal/mnemonic/mnemonic.go b/internal/mnemonic/mnemonic.go index c276074..0ebdeb0 100644 --- a/internal/mnemonic/mnemonic.go +++ b/internal/mnemonic/mnemonic.go @@ -63,6 +63,7 @@ func Read(ctx context.Context, command string) (string, error) { func run(ctx context.Context, command string) (string, error) { //nolint:gosec // running the user's own command is the point shell := exec.CommandContext(ctx, "sh", "-c", command) + shell.Env = ChildEnv() var complaint bytes.Buffer @@ -83,6 +84,25 @@ func run(ctx context.Context, command string) (string, error) { return string(out), nil } +// ChildEnv is the tool's environment with the mnemonic variables taken +// out, for the programs the tool starts: the mnemonic command, ssh and +// sftp. The mnemonic, from either variable, never leaves the tool. +func ChildEnv() []string { + environ := os.Environ() + kept := make([]string, 0, len(environ)) + + for _, entry := range environ { + name, _, _ := strings.Cut(entry, "=") + if name == Variable || name == CommandVariable { + continue + } + + kept = append(kept, entry) + } + + return kept +} + // ask prompts on the terminal with echo turned off. func ask() (string, error) { fd := int(os.Stdin.Fd()) diff --git a/internal/mnemonic/mnemonic_test.go b/internal/mnemonic/mnemonic_test.go index 7924f90..97ea3cb 100644 --- a/internal/mnemonic/mnemonic_test.go +++ b/internal/mnemonic/mnemonic_test.go @@ -1,6 +1,8 @@ package mnemonic_test import ( + "os" + "path/filepath" "strings" "testing" @@ -61,6 +63,31 @@ func TestTheMnemonicInTheEnvironmentComesLast(t *testing.T) { require.Equal(t, fromVariable, words) } +func TestTheMnemonicIsNotHandedToTheCommand(t *testing.T) { + const marker = "KEYFUNC_TEST_MARKER" + + t.Setenv(mnemonic.CommandVariable, prints(fromCommandVariable)) + t.Setenv(mnemonic.Variable, fromVariable) + t.Setenv(marker, "reaches the command") + + path := filepath.Join(t.TempDir(), "environment") + + words, err := mnemonic.Read( + t.Context(), "env > '"+path+"'; "+prints(example()), + ) + require.NoError(t, err) + require.Equal(t, example(), words) + + //nolint:gosec // the path is a temporary file of the test's own + content, err := os.ReadFile(path) + require.NoError(t, err) + + environment := string(content) + require.NotContains(t, environment, mnemonic.Variable+"=") + require.NotContains(t, environment, mnemonic.CommandVariable+"=") + require.Contains(t, environment, marker+"=") +} + func TestAFailingCommandSaysWhatWentWrong(t *testing.T) { t.Setenv(mnemonic.CommandVariable, "") t.Setenv(mnemonic.Variable, "")