Derive from the mnemonic's words joined by single spaces (closes #49)
check / check (push) Successful in 2m21s

The BIP-39 seed was computed over the mnemonic string as given, with only its ends trimmed, so the same words one per line, tab-separated or double-spaced passed the checksum but gave different keys with no warning. The words are now joined by single spaces before the checksum and the seed, for every source: the mnemonic command, both environment variables and the prompt. Single-spaced input gives the same keys as before; a test checks the README vector for each spacing.

Model: opus-5-5
This commit was merged in pull request #53.
This commit is contained in:
2026-10-04 07:08:46 +02:00
parent 1ddc2c747a
commit 897b43a206
3 changed files with 28 additions and 7 deletions
+5 -4
View File
@@ -106,9 +106,8 @@ order; the first one found wins:
1. `--mnemonic-command <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`
+19
View File
@@ -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())
+4 -3
View File
@@ -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