diff --git a/README.md b/README.md index fda0e0a..464f8ed 100644 --- a/README.md +++ b/README.md @@ -106,9 +106,8 @@ order; the first one found wins: 1. `--mnemonic-command `: a shell command, run with `sh -c`, whose standard output is the mnemonic. Example: - `--mnemonic-command 'secret get foo'`. Whitespace around the output is - dropped. If the command exits with a non-zero status, the tool prints its - standard error and exits with status 1. + `--mnemonic-command 'secret get foo'`. If the command exits with a non-zero + status, the tool prints its standard error and exits with status 1. 2. Environment variable `KEYFUNC_MNEMONIC_COMMAND`: the same, as a shell command held in the environment. 3. Environment variable `KEYFUNC_MNEMONIC`: the mnemonic itself. @@ -116,7 +115,9 @@ order; the first one found wins: If none of these is available and standard input is not a terminal, the tool refuses and exits with status 1. A mnemonic that fails the BIP-39 checksum is -refused with a message saying so. +refused with a message saying so. Keys are derived from the mnemonic's words +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` diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go index 2f46688..f4146bc 100644 --- a/internal/cli/cli_test.go +++ b/internal/cli/cli_test.go @@ -47,6 +47,25 @@ func TestTheReadmeTestVectors(t *testing.T) { ) } +func TestTheSpacingBetweenTheWordsDoesNotChangeTheKeys(t *testing.T) { + words := strings.Fields(example()) + + for name, spaced := range map[string]string{ + "one word per line": strings.Join(words, "\n"), + "double spaces": strings.Join(words, " "), + "tabs": strings.Join(words, "\t"), + } { + t.Run(name, func(t *testing.T) { + t.Setenv(mnemonic.Variable, spaced) + + require.Equal(t, + vectorZero+" keyfunc/ssh/0", + strings.TrimSpace(run(t, "ssh", "pub", "-n", "0")), + ) + }) + } +} + func TestTheCommentCanBeChosen(t *testing.T) { t.Setenv(mnemonic.Variable, example()) diff --git a/internal/mnemonic/mnemonic.go b/internal/mnemonic/mnemonic.go index 85b34c0..fdce914 100644 --- a/internal/mnemonic/mnemonic.go +++ b/internal/mnemonic/mnemonic.go @@ -104,10 +104,11 @@ func ask() (string, error) { return checked(string(typed)) } -// checked drops the surrounding whitespace and refuses a mnemonic that -// does not pass the BIP-39 checksum. +// checked joins the words with single spaces, whatever whitespace +// separated them, since the seed is computed over the string itself, +// and refuses a mnemonic that does not pass the BIP-39 checksum. func checked(words string) (string, error) { - words = strings.TrimSpace(words) + words = strings.Join(strings.Fields(words), " ") if !bip39.IsMnemonicValid(words) { return "", ErrChecksum