Keep the mnemonic variables from the mnemonic command (closes #80) #83

Merged
clawbot merged 1 commits from issue-80-mnemonic-command-env into next 2026-10-14 20:48:22 +02:00
6 changed files with 54 additions and 27 deletions
+3 -2
View File
@@ -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. word per line, tabs or extra spaces give the same keys.
`KEYFUNC_MNEMONIC` and `KEYFUNC_MNEMONIC_COMMAND` are removed from the `KEYFUNC_MNEMONIC` and `KEYFUNC_MNEMONIC_COMMAND` are removed from the
environment before the system `ssh` (`keyfunc ssh to`) and `sftp` environment before the mnemonic command, the system `ssh` (`keyfunc ssh to`) and
(`keyfunc ssh install`) are started, so the mnemonic is never handed on to them. `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`. Every command takes `--index` / `-n` and `--mnemonic-command`, and has `--help`.
`keyfunc --version` prints the version. `make build` stamps it; a binary `keyfunc --version` prints the version. `make build` stamps it; a binary
+2 -1
View File
@@ -16,6 +16,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"sneak.berlin/go/keyfunc/internal/cli/signals" "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 // 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 //nolint:gosec // the options are the user's own, meant for sftp
command := exec.CommandContext(cmd.Context(), "sftp", argv...) command := exec.CommandContext(cmd.Context(), "sftp", argv...)
command.Env = childEnv() command.Env = mnemonic.ChildEnv()
command.Stdin = strings.NewReader(strings.Join(batch, "\n") + "\n") command.Stdin = strings.NewReader(strings.Join(batch, "\n") + "\n")
command.Stdout = &said command.Stdout = &said
command.Stderr = &said command.Stderr = &said
-23
View File
@@ -3,13 +3,10 @@ package ssh
import ( import (
"fmt" "fmt"
"os"
"strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"sneak.berlin/go/keyfunc/internal/cli/options" "sneak.berlin/go/keyfunc/internal/cli/options"
"sneak.berlin/go/keyfunc/internal/derive" "sneak.berlin/go/keyfunc/internal/derive"
"sneak.berlin/go/keyfunc/internal/mnemonic"
"sneak.berlin/go/keyfunc/internal/sshkey" "sneak.berlin/go/keyfunc/internal/sshkey"
) )
@@ -87,26 +84,6 @@ func write(cmd *cobra.Command, text string) error {
return nil 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. // addComment gives a command its comment flag.
func addComment(cmd *cobra.Command) { func addComment(cmd *cobra.Command) {
cmd.Flags().String( cmd.Flags().String(
+2 -1
View File
@@ -11,6 +11,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"sneak.berlin/go/keyfunc/internal/cli/signals" "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. // 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 { func connect(ctx context.Context, argv []string) error {
//nolint:gosec // the arguments are the user's own, meant for ssh //nolint:gosec // the arguments are the user's own, meant for ssh
command := exec.CommandContext(ctx, "ssh", argv...) command := exec.CommandContext(ctx, "ssh", argv...)
command.Env = childEnv() command.Env = mnemonic.ChildEnv()
command.Stdin = os.Stdin command.Stdin = os.Stdin
command.Stdout = os.Stdout command.Stdout = os.Stdout
command.Stderr = os.Stderr command.Stderr = os.Stderr
+20
View File
@@ -63,6 +63,7 @@ func Read(ctx context.Context, command string) (string, error) {
func run(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 //nolint:gosec // running the user's own command is the point
shell := exec.CommandContext(ctx, "sh", "-c", command) shell := exec.CommandContext(ctx, "sh", "-c", command)
shell.Env = ChildEnv()
var complaint bytes.Buffer var complaint bytes.Buffer
@@ -83,6 +84,25 @@ func run(ctx context.Context, command string) (string, error) {
return string(out), nil 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. // ask prompts on the terminal with echo turned off.
func ask() (string, error) { func ask() (string, error) {
fd := int(os.Stdin.Fd()) fd := int(os.Stdin.Fd())
+27
View File
@@ -1,6 +1,8 @@
package mnemonic_test package mnemonic_test
import ( import (
"os"
"path/filepath"
"strings" "strings"
"testing" "testing"
@@ -61,6 +63,31 @@ func TestTheMnemonicInTheEnvironmentComesLast(t *testing.T) {
require.Equal(t, fromVariable, words) 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) { func TestAFailingCommandSaysWhatWentWrong(t *testing.T) {
t.Setenv(mnemonic.CommandVariable, "") t.Setenv(mnemonic.CommandVariable, "")
t.Setenv(mnemonic.Variable, "") t.Setenv(mnemonic.Variable, "")