Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
261c817750 |
@@ -119,19 +119,17 @@ order; the first one found wins:
|
|||||||
4. A prompt on the terminal with echo turned off.
|
4. A prompt on the terminal with echo turned off.
|
||||||
|
|
||||||
If none of these is available and standard input is not a terminal, the tool
|
If none of these is available and standard input is not a terminal, the tool
|
||||||
refuses and exits with status 1. A mnemonic that is empty, does not have 12, 15,
|
refuses and exits with status 1. A mnemonic that fails the BIP-39 checksum is
|
||||||
18, 21 or 24 words, has a word that is not on the BIP-39 word list, or fails the
|
refused with a message saying so. Keys are derived from the mnemonic's words
|
||||||
BIP-39 checksum is refused with a message saying which. A word that is not on
|
joined by single spaces, whatever whitespace is around or between them, so one
|
||||||
the list is named by its position, never printed. Keys are derived from the
|
word per line, tabs or extra spaces give the same keys.
|
||||||
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.
|
|
||||||
|
|
||||||
The key index, `--words`, the `--to` recipients, the file `age encrypt` or
|
The key index, `--words`, the `--to` recipients, the file `age encrypt` or
|
||||||
`age decrypt` is given to read and its `-o` path are checked before the mnemonic
|
`age decrypt` is given to read and its `-o` path are checked before the mnemonic
|
||||||
is looked for, so a run refused for one of them never prompts for the mnemonic
|
is looked for, so a run refused for one of them never prompts for the mnemonic
|
||||||
or runs the mnemonic command. An `-o` path is refused at that point if it is a
|
or runs the mnemonic command. An `-o` path is refused at that point if its
|
||||||
directory or a symlink to one, if its directory is missing or is not a
|
directory is missing or is not a directory, or if it is a symlink that points at
|
||||||
directory, or if it is a symlink that points at nothing.
|
nothing; anything else wrong with it shows only when the output is written.
|
||||||
|
|
||||||
`KEYFUNC_MNEMONIC` and `KEYFUNC_MNEMONIC_COMMAND` are removed from the
|
`KEYFUNC_MNEMONIC` and `KEYFUNC_MNEMONIC_COMMAND` are removed from the
|
||||||
environment before the mnemonic command, the system `ssh` (`keyfunc ssh to`) and
|
environment before the mnemonic command, the system `ssh` (`keyfunc ssh to`) and
|
||||||
|
|||||||
+4
-12
@@ -11,7 +11,6 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"sneak.berlin/go/keyfunc/internal/agekey"
|
"sneak.berlin/go/keyfunc/internal/agekey"
|
||||||
@@ -237,10 +236,9 @@ func input(cmd *cobra.Command, args []string) (io.Reader, func(), error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// checkOutput refuses an -o path that output could not write to,
|
// checkOutput refuses an -o path that output could not write to,
|
||||||
// before anything is written: a directory or a symlink to one, one
|
// before anything is written: one whose directory is missing or is not
|
||||||
// whose directory is missing or is not a directory, or a symlink that
|
// a directory, or a symlink that points at nothing. It creates nothing;
|
||||||
// points at nothing. It creates nothing; output looks at the path again
|
// output looks at the path again when it writes.
|
||||||
// when it writes.
|
|
||||||
func checkOutput(cmd *cobra.Command) error {
|
func checkOutput(cmd *cobra.Command) error {
|
||||||
name, err := cmd.Flags().GetString("output")
|
name, err := cmd.Flags().GetString("output")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -264,21 +262,15 @@ func checkOutput(cmd *cobra.Command) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("looking at %s: %w", directory, err)
|
return fmt.Errorf("looking at %s: %w", directory, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
|
||||||
case err != nil:
|
case err != nil:
|
||||||
return fmt.Errorf("looking at %s: %w", name, err)
|
return fmt.Errorf("looking at %s: %w", name, err)
|
||||||
case info.Mode().Type() == fs.ModeSymlink:
|
case info.Mode().Type() == fs.ModeSymlink:
|
||||||
info, err = os.Stat(name)
|
_, err = os.Stat(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("following %s: %w", name, err)
|
return fmt.Errorf("following %s: %w", name, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if info.IsDir() {
|
|
||||||
return fmt.Errorf("writing to %s: %w", name, syscall.EISDIR)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -119,15 +119,10 @@ func TestAnOutputPathThatCannotBeWrittenIsRefused(t *testing.T) {
|
|||||||
dangling := filepath.Join(directory, "dangling")
|
dangling := filepath.Join(directory, "dangling")
|
||||||
require.NoError(t, os.Symlink(missing, dangling))
|
require.NoError(t, os.Symlink(missing, dangling))
|
||||||
|
|
||||||
toDirectory := filepath.Join(directory, "to-directory")
|
|
||||||
require.NoError(t, os.Symlink(t.TempDir(), toDirectory))
|
|
||||||
|
|
||||||
refusals := map[string]error{
|
refusals := map[string]error{
|
||||||
filepath.Join(missing, "notes.age"): fs.ErrNotExist,
|
filepath.Join(missing, "notes.age"): fs.ErrNotExist,
|
||||||
filepath.Join(plain, "notes.age"): syscall.ENOTDIR,
|
filepath.Join(plain, "notes.age"): syscall.ENOTDIR,
|
||||||
dangling: fs.ErrNotExist,
|
dangling: fs.ErrNotExist,
|
||||||
directory: syscall.EISDIR,
|
|
||||||
toDirectory: syscall.EISDIR,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, refusal := range refusals {
|
for name, refusal := range refusals {
|
||||||
@@ -140,10 +135,10 @@ func TestAnOutputPathThatCannotBeWrittenIsRefused(t *testing.T) {
|
|||||||
require.Empty(t, out, name)
|
require.Empty(t, out, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nothing was made beside the two symlinks.
|
// Nothing was made beside the symlink.
|
||||||
left, err := os.ReadDir(directory)
|
left, err := os.ReadDir(directory)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, left, 2)
|
require.Len(t, left, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAFileForAnotherKeyIsRefused(t *testing.T) {
|
func TestAFileForAnotherKeyIsRefused(t *testing.T) {
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"slices"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"golang.org/x/term"
|
"golang.org/x/term"
|
||||||
@@ -30,28 +29,6 @@ var ErrMissing = errors.New(
|
|||||||
"no mnemonic given and standard input is not a terminal",
|
"no mnemonic given and standard input is not a terminal",
|
||||||
)
|
)
|
||||||
|
|
||||||
// The lengths a BIP-39 mnemonic may have.
|
|
||||||
const (
|
|
||||||
twelve = 12
|
|
||||||
fifteen = 15
|
|
||||||
eighteen = 18
|
|
||||||
twentyOne = 21
|
|
||||||
twentyFour = 24
|
|
||||||
)
|
|
||||||
|
|
||||||
// ErrEmpty is returned for a mnemonic with no words in it, such as the
|
|
||||||
// output of a mnemonic command that printed nothing.
|
|
||||||
var ErrEmpty = errors.New("the mnemonic is empty")
|
|
||||||
|
|
||||||
// ErrWordCount is returned for a mnemonic with a number of words that
|
|
||||||
// BIP-39 does not allow.
|
|
||||||
var ErrWordCount = errors.New("a mnemonic has 12, 15, 18, 21 or 24 words")
|
|
||||||
|
|
||||||
// ErrUnknownWord is returned for a mnemonic with a word that is not on
|
|
||||||
// the BIP-39 English word list. The error gives the word's position,
|
|
||||||
// never the word, so the mnemonic stays out of the message.
|
|
||||||
var ErrUnknownWord = errors.New("not on the BIP-39 word list")
|
|
||||||
|
|
||||||
// ErrChecksum is returned for a mnemonic that fails the BIP-39
|
// ErrChecksum is returned for a mnemonic that fails the BIP-39
|
||||||
// checksum.
|
// checksum.
|
||||||
var ErrChecksum = errors.New("the mnemonic fails its BIP-39 checksum")
|
var ErrChecksum = errors.New("the mnemonic fails its BIP-39 checksum")
|
||||||
@@ -149,34 +126,13 @@ func ask() (string, error) {
|
|||||||
|
|
||||||
// checked joins the words with single spaces, whatever whitespace
|
// checked joins the words with single spaces, whatever whitespace
|
||||||
// separated them, since the seed is computed over the string itself,
|
// separated them, since the seed is computed over the string itself,
|
||||||
// and refuses a mnemonic that is not a valid BIP-39 mnemonic, with an
|
// and refuses a mnemonic that does not pass the BIP-39 checksum.
|
||||||
// error that says what is wrong with it.
|
func checked(words string) (string, error) {
|
||||||
func checked(text string) (string, error) {
|
words = strings.Join(strings.Fields(words), " ")
|
||||||
words := strings.Fields(text)
|
|
||||||
|
|
||||||
switch len(words) {
|
if !bip39.IsMnemonicValid(words) {
|
||||||
case 0:
|
|
||||||
return "", ErrEmpty
|
|
||||||
case twelve, fifteen, eighteen, twentyOne, twentyFour:
|
|
||||||
default:
|
|
||||||
return "", fmt.Errorf("%w, not %d", ErrWordCount, len(words))
|
|
||||||
}
|
|
||||||
|
|
||||||
wordList := bip39.English()
|
|
||||||
|
|
||||||
for i, word := range words {
|
|
||||||
if !slices.Contains(wordList, word) {
|
|
||||||
return "", fmt.Errorf(
|
|
||||||
"word %d of the mnemonic is %w", i+1, ErrUnknownWord,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
joined := strings.Join(words, " ")
|
|
||||||
|
|
||||||
if !bip39.IsMnemonicValid(joined) {
|
|
||||||
return "", ErrChecksum
|
return "", ErrChecksum
|
||||||
}
|
}
|
||||||
|
|
||||||
return joined, nil
|
return words, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,27 +97,6 @@ func TestAFailingCommandSaysWhatWentWrong(t *testing.T) {
|
|||||||
require.Contains(t, err.Error(), "nothing here")
|
require.Contains(t, err.Error(), "nothing here")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestValidMnemonicsOf15To24WordsAreAccepted(t *testing.T) {
|
|
||||||
t.Setenv(mnemonic.CommandVariable, "")
|
|
||||||
|
|
||||||
// abandon repeated, then the last word that makes the checksum
|
|
||||||
// match: 15, 18, 21 and 24 words.
|
|
||||||
valid := []string{
|
|
||||||
strings.Repeat("abandon ", 14) + "address",
|
|
||||||
strings.Repeat("abandon ", 17) + "agent",
|
|
||||||
strings.Repeat("abandon ", 20) + "admit",
|
|
||||||
strings.Repeat("abandon ", 23) + "art",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, words := range valid {
|
|
||||||
t.Setenv(mnemonic.Variable, words)
|
|
||||||
|
|
||||||
got, err := mnemonic.Read(t.Context(), "")
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, words, got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestABadChecksumIsRefused(t *testing.T) {
|
func TestABadChecksumIsRefused(t *testing.T) {
|
||||||
t.Setenv(mnemonic.CommandVariable, "")
|
t.Setenv(mnemonic.CommandVariable, "")
|
||||||
t.Setenv(mnemonic.Variable, broken())
|
t.Setenv(mnemonic.Variable, broken())
|
||||||
@@ -126,38 +105,6 @@ func TestABadChecksumIsRefused(t *testing.T) {
|
|||||||
require.ErrorIs(t, err, mnemonic.ErrChecksum)
|
require.ErrorIs(t, err, mnemonic.ErrChecksum)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestACommandThatPrintsNothingGivesAnEmptyMnemonic(t *testing.T) {
|
|
||||||
t.Setenv(mnemonic.CommandVariable, "")
|
|
||||||
t.Setenv(mnemonic.Variable, "")
|
|
||||||
|
|
||||||
_, err := mnemonic.Read(t.Context(), "true")
|
|
||||||
require.ErrorIs(t, err, mnemonic.ErrEmpty)
|
|
||||||
require.NotContains(t, err.Error(), "checksum")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAWrongWordCountIsRefused(t *testing.T) {
|
|
||||||
t.Setenv(mnemonic.CommandVariable, "")
|
|
||||||
t.Setenv(mnemonic.Variable, strings.Repeat("abandon ", 3))
|
|
||||||
|
|
||||||
_, err := mnemonic.Read(t.Context(), "")
|
|
||||||
require.ErrorIs(t, err, mnemonic.ErrWordCount)
|
|
||||||
require.ErrorContains(t, err, "not 3")
|
|
||||||
require.NotContains(t, err.Error(), "checksum")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAWordNotOnTheListIsNamedByPositionOnly(t *testing.T) {
|
|
||||||
const typo = "abuot"
|
|
||||||
|
|
||||||
t.Setenv(mnemonic.CommandVariable, "")
|
|
||||||
t.Setenv(mnemonic.Variable, strings.Repeat("abandon ", 11)+typo)
|
|
||||||
|
|
||||||
_, err := mnemonic.Read(t.Context(), "")
|
|
||||||
require.ErrorIs(t, err, mnemonic.ErrUnknownWord)
|
|
||||||
require.ErrorContains(t, err, "word 12 ")
|
|
||||||
require.NotContains(t, err.Error(), typo)
|
|
||||||
require.NotContains(t, err.Error(), "checksum")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNothingToReadAndNobodyToAsk(t *testing.T) {
|
func TestNothingToReadAndNobodyToAsk(t *testing.T) {
|
||||||
t.Setenv(mnemonic.CommandVariable, "")
|
t.Setenv(mnemonic.CommandVariable, "")
|
||||||
t.Setenv(mnemonic.Variable, "")
|
t.Setenv(mnemonic.Variable, "")
|
||||||
|
|||||||
Reference in New Issue
Block a user