Check arguments before looking for the mnemonic (closes #79)
check / check (push) Waiting to run
check / check (push) Waiting to run
The key index, --words, the --to recipients and the input file of age encrypt and age decrypt were checked only after the mnemonic had been obtained, so a run that could not succeed still prompted for it or ran the mnemonic command. options.Index now refuses an index above derive.MaxIndex, which covers every command group; the mnemonic command calls the new childmnemonic.CheckWords first; age encrypt reads its recipients with the new agekey.Recipients, and both age commands open their input, before the key is derived. Encrypt now takes the parsed recipients. The README says which checks come first. Model: opus-5-5
This commit is contained in:
@@ -124,6 +124,11 @@ 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
|
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.
|
||||||
|
|
||||||
|
The key index, `--words`, the `--to` recipients and the file `age encrypt` or
|
||||||
|
`age decrypt` is given to read are checked before the mnemonic is looked for, so
|
||||||
|
a run refused for one of them never prompts for the mnemonic or runs the
|
||||||
|
mnemonic command.
|
||||||
|
|
||||||
`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 system `ssh` (`keyfunc ssh to`) and `sftp`
|
||||||
(`keyfunc ssh install`) are started, so the mnemonic is never handed on to them.
|
(`keyfunc ssh install`) are started, so the mnemonic is never handed on to them.
|
||||||
|
|||||||
@@ -94,15 +94,12 @@ func (k *Key) Identity() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Encrypt copies src to dst, encrypted to this key and to every extra
|
// Encrypt copies src to dst, encrypted to this key and to every extra
|
||||||
// recipient named, so the same mnemonic can always read it back again.
|
// recipient given, so the same mnemonic can always read it back again.
|
||||||
// Armored output is the text form age also reads.
|
// Armored output is the text form age also reads.
|
||||||
func (k *Key) Encrypt(
|
func (k *Key) Encrypt(
|
||||||
dst io.Writer, src io.Reader, to []string, armored bool,
|
dst io.Writer, src io.Reader, to []age.Recipient, armored bool,
|
||||||
) error {
|
) error {
|
||||||
all, err := recipients(k.identity.Recipient(), to)
|
all := append([]age.Recipient{k.identity.Recipient()}, to...)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
out := dst
|
out := dst
|
||||||
|
|
||||||
@@ -113,7 +110,7 @@ func (k *Key) Encrypt(
|
|||||||
out = text
|
out = text
|
||||||
}
|
}
|
||||||
|
|
||||||
err = encrypt(out, src, all)
|
err := encrypt(out, src, all)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -130,12 +127,12 @@ func (k *Key) Encrypt(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// recipients returns the key's own recipient followed by the ones
|
// Recipients reads the recipients named on the command line, for
|
||||||
// named on the command line, so what the key encrypts it can read.
|
// Encrypt to add to the key's own.
|
||||||
func recipients(mine age.Recipient, to []string) ([]age.Recipient, error) {
|
func Recipients(names []string) ([]age.Recipient, error) {
|
||||||
list := []age.Recipient{mine}
|
list := make([]age.Recipient, 0, len(names))
|
||||||
|
|
||||||
for _, name := range to {
|
for _, name := range names {
|
||||||
parsed, err := age.ParseX25519Recipient(name)
|
parsed, err := age.ParseX25519Recipient(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("reading recipient %q: %w", name, err)
|
return nil, fmt.Errorf("reading recipient %q: %w", name, err)
|
||||||
|
|||||||
@@ -119,12 +119,12 @@ func TestAnExtraRecipientCanReadItTooAndSoCanTheDerivedOne(t *testing.T) {
|
|||||||
|
|
||||||
mine, theirs := forIndex(t, 0), forIndex(t, 1)
|
mine, theirs := forIndex(t, 0), forIndex(t, 1)
|
||||||
|
|
||||||
|
to, err := agekey.Recipients([]string{theirs.Recipient()})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
var sealed bytes.Buffer
|
var sealed bytes.Buffer
|
||||||
|
|
||||||
err := mine.Encrypt(
|
err = mine.Encrypt(&sealed, strings.NewReader("hello"), to, false)
|
||||||
&sealed, strings.NewReader("hello"),
|
|
||||||
[]string{theirs.Recipient()}, false,
|
|
||||||
)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
for _, key := range []*agekey.Key{mine, theirs} {
|
for _, key := range []*agekey.Key{mine, theirs} {
|
||||||
@@ -138,10 +138,7 @@ func TestAnExtraRecipientCanReadItTooAndSoCanTheDerivedOne(t *testing.T) {
|
|||||||
func TestARecipientThatIsNotOneIsRefused(t *testing.T) {
|
func TestARecipientThatIsNotOneIsRefused(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
err := forIndex(t, 0).Encrypt(
|
_, err := agekey.Recipients([]string{"not a recipient"})
|
||||||
&bytes.Buffer{}, strings.NewReader("hello"),
|
|
||||||
[]string{"not a recipient"}, false,
|
|
||||||
)
|
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,17 @@ const DefaultWords = twelve
|
|||||||
// ErrWordCount is returned for a length this tool does not offer.
|
// ErrWordCount is returned for a length this tool does not offer.
|
||||||
var ErrWordCount = errors.New("a child mnemonic has 12, 18 or 24 words")
|
var ErrWordCount = errors.New("a child mnemonic has 12, 18 or 24 words")
|
||||||
|
|
||||||
|
// CheckWords refuses a length this tool does not offer, so that the
|
||||||
|
// command can turn it down before it looks for the mnemonic.
|
||||||
|
func CheckWords(words uint32) error {
|
||||||
|
switch words {
|
||||||
|
case twelve, eighteen, twentyFour:
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("%w, not %d", ErrWordCount, words)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Derive returns the English child mnemonic of that many words at that
|
// Derive returns the English child mnemonic of that many words at that
|
||||||
// key index, taken from the master key with BIP-85's own mnemonic
|
// key index, taken from the master key with BIP-85's own mnemonic
|
||||||
// application, number 39. The words come straight from the BIP-85
|
// application, number 39. The words come straight from the BIP-85
|
||||||
@@ -38,13 +49,12 @@ func Derive(
|
|||||||
master *hdkeychain.ExtendedKey,
|
master *hdkeychain.ExtendedKey,
|
||||||
words, index uint32,
|
words, index uint32,
|
||||||
) (string, error) {
|
) (string, error) {
|
||||||
switch words {
|
err := CheckWords(words)
|
||||||
case twelve, eighteen, twentyFour:
|
if err != nil {
|
||||||
default:
|
return "", err
|
||||||
return "", fmt.Errorf("%w, not %d", ErrWordCount, words)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err := derive.CheckIndex(index)
|
err = derive.CheckIndex(index)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|||||||
+47
-31
@@ -108,47 +108,25 @@ func decrypt() *cobra.Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// runEncrypt encrypts to the derived recipient and any others given.
|
// runEncrypt encrypts to the derived recipient and any others given.
|
||||||
|
// The recipients are read and the input opened before the key is
|
||||||
|
// derived, so that a run refused for either never asks for the
|
||||||
|
// mnemonic.
|
||||||
func runEncrypt(cmd *cobra.Command, args []string) error {
|
func runEncrypt(cmd *cobra.Command, args []string) error {
|
||||||
key, err := derived(cmd)
|
names, err := cmd.Flags().GetStringArray("to")
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
to, err := cmd.Flags().GetStringArray("to")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("reading the recipients: %w", err)
|
return fmt.Errorf("reading the recipients: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
to, err := agekey.Recipients(names)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
armored, err := cmd.Flags().GetBool("armor")
|
armored, err := cmd.Flags().GetBool("armor")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("reading the armor flag: %w", err)
|
return fmt.Errorf("reading the armor flag: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return through(cmd, args, func(dst io.Writer, src io.Reader) error {
|
|
||||||
return key.Encrypt(dst, src, to, armored)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// runDecrypt decrypts with the derived identity.
|
|
||||||
func runDecrypt(cmd *cobra.Command, args []string) error {
|
|
||||||
key, err := derived(cmd)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return through(cmd, args, key.Decrypt)
|
|
||||||
}
|
|
||||||
|
|
||||||
// through opens the input the arguments ask for and hands it to the
|
|
||||||
// work, with the file --output names to write to, or the command's own
|
|
||||||
// output when it names none or names the same file as that output, and
|
|
||||||
// the command's own error output when it names the same file as that.
|
|
||||||
// Those two are the streams the tool already has, so whatever they are
|
|
||||||
// redirected to is written as the redirect says, never replaced.
|
|
||||||
func through(
|
|
||||||
cmd *cobra.Command, args []string,
|
|
||||||
work func(io.Writer, io.Reader) error,
|
|
||||||
) error {
|
|
||||||
src, closeSrc, err := input(cmd, args)
|
src, closeSrc, err := input(cmd, args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -156,6 +134,44 @@ func through(
|
|||||||
|
|
||||||
defer closeSrc()
|
defer closeSrc()
|
||||||
|
|
||||||
|
key, err := derived(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return through(cmd, src, func(dst io.Writer, plain io.Reader) error {
|
||||||
|
return key.Encrypt(dst, plain, to, armored)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// runDecrypt decrypts with the derived identity. The input is opened
|
||||||
|
// before the key is derived, as for runEncrypt.
|
||||||
|
func runDecrypt(cmd *cobra.Command, args []string) error {
|
||||||
|
src, closeSrc, err := input(cmd, args)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer closeSrc()
|
||||||
|
|
||||||
|
key, err := derived(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return through(cmd, src, key.Decrypt)
|
||||||
|
}
|
||||||
|
|
||||||
|
// through hands the input to the work, with the file --output names to
|
||||||
|
// write to, or the command's own output when it names none or names the
|
||||||
|
// same file as that output, and the command's own error output when it
|
||||||
|
// names the same file as that. Those two are the streams the tool
|
||||||
|
// already has, so whatever they are redirected to is written as the
|
||||||
|
// redirect says, never replaced.
|
||||||
|
func through(
|
||||||
|
cmd *cobra.Command, src io.Reader,
|
||||||
|
work func(io.Writer, io.Reader) error,
|
||||||
|
) error {
|
||||||
name, err := cmd.Flags().GetString("output")
|
name, err := cmd.Flags().GetString("output")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("reading the output file: %w", err)
|
return fmt.Errorf("reading the output file: %w", err)
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import (
|
|||||||
"sneak.berlin/go/keyfunc/internal/agekey"
|
"sneak.berlin/go/keyfunc/internal/agekey"
|
||||||
"sneak.berlin/go/keyfunc/internal/cli"
|
"sneak.berlin/go/keyfunc/internal/cli"
|
||||||
"sneak.berlin/go/keyfunc/internal/cli/age"
|
"sneak.berlin/go/keyfunc/internal/cli/age"
|
||||||
|
"sneak.berlin/go/keyfunc/internal/derive"
|
||||||
"sneak.berlin/go/keyfunc/internal/mnemonic"
|
"sneak.berlin/go/keyfunc/internal/mnemonic"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -69,6 +70,44 @@ func TestAnotherRecipientIsAddedAndTheDerivedOneStays(t *testing.T) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The mnemonic command fails in these runs, so the refusal of the index
|
||||||
|
// shows that it was checked before the mnemonic was looked for.
|
||||||
|
func TestAnAgeIndexWithNoHardenedChildIsRefused(t *testing.T) {
|
||||||
|
t.Setenv(mnemonic.CommandVariable, failingMnemonicCommand)
|
||||||
|
|
||||||
|
for _, command := range []string{"pub", "priv", "encrypt", "decrypt"} {
|
||||||
|
out, err := execute(t, "age", command, "-n", tooLarge)
|
||||||
|
require.ErrorIs(t, err, derive.ErrIndexTooLarge, command)
|
||||||
|
require.Empty(t, out, command)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The mnemonic command fails in this run, so the refusal of the
|
||||||
|
// recipient shows that it was read before the mnemonic was looked for.
|
||||||
|
func TestAnEncryptionToARecipientThatIsNotOneIsRefused(t *testing.T) {
|
||||||
|
t.Setenv(mnemonic.CommandVariable, failingMnemonicCommand)
|
||||||
|
|
||||||
|
plain := written(t, "notes.txt", "the secret\n")
|
||||||
|
|
||||||
|
out, err := execute(t, "age", "encrypt", "--to", "bogus", plain)
|
||||||
|
require.ErrorContains(t, err, `reading recipient "bogus"`)
|
||||||
|
require.Empty(t, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The mnemonic command fails in these runs, so the refusal of the input
|
||||||
|
// file shows that it was opened before the mnemonic was looked for.
|
||||||
|
func TestAnInputFileThatCannotBeOpenedIsRefused(t *testing.T) {
|
||||||
|
t.Setenv(mnemonic.CommandVariable, failingMnemonicCommand)
|
||||||
|
|
||||||
|
missing := filepath.Join(t.TempDir(), "missing")
|
||||||
|
|
||||||
|
for _, operation := range []string{"encrypt", "decrypt"} {
|
||||||
|
out, err := execute(t, "age", operation, missing)
|
||||||
|
require.ErrorIs(t, err, fs.ErrNotExist, operation)
|
||||||
|
require.Empty(t, out, operation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestAFileForAnotherKeyIsRefused(t *testing.T) {
|
func TestAFileForAnotherKeyIsRefused(t *testing.T) {
|
||||||
t.Setenv(mnemonic.Variable, example())
|
t.Setenv(mnemonic.Variable, example())
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,14 @@ const (
|
|||||||
twentyFour = 24
|
twentyFour = 24
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// tooLarge is the smallest key index with no hardened child.
|
||||||
|
const tooLarge = "2147483648"
|
||||||
|
|
||||||
|
// failingMnemonicCommand fails loudly, so a run that looks for the
|
||||||
|
// mnemonic ends with that failure. A run that ends with the refusal of
|
||||||
|
// an argument instead shows that the argument was checked first.
|
||||||
|
const failingMnemonicCommand = "echo the mnemonic was looked for >&2; exit 1"
|
||||||
|
|
||||||
// example returns the mnemonic the README gives its test vectors for:
|
// example returns the mnemonic the README gives its test vectors for:
|
||||||
// eleven abandons and about.
|
// eleven abandons and about.
|
||||||
func example() string {
|
func example() string {
|
||||||
@@ -99,12 +107,25 @@ func TestThePrivateKeyMatchesThePublicOne(t *testing.T) {
|
|||||||
require.Equal(t, vectorOne, back)
|
require.Equal(t, vectorOne, back)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The mnemonic command fails in these runs, so the refusal of the index
|
||||||
|
// shows that it was checked before the mnemonic was looked for. The age
|
||||||
|
// commands have a test of their own.
|
||||||
func TestAnIndexWithNoHardenedChildIsRefused(t *testing.T) {
|
func TestAnIndexWithNoHardenedChildIsRefused(t *testing.T) {
|
||||||
t.Setenv(mnemonic.Variable, example())
|
t.Setenv(mnemonic.CommandVariable, failingMnemonicCommand)
|
||||||
|
|
||||||
out, err := execute(t, "ssh", "pub", "-n", "2147483648")
|
for _, args := range [][]string{
|
||||||
require.ErrorIs(t, err, derive.ErrIndexTooLarge)
|
{subcommand, "pub", "-n", tooLarge},
|
||||||
require.Empty(t, out)
|
{subcommand, "priv", "-n", tooLarge},
|
||||||
|
{subcommand, installing, "-n", tooLarge, host},
|
||||||
|
{subcommand, "to", "-n", tooLarge, host},
|
||||||
|
{"mnemonic", "-n", tooLarge},
|
||||||
|
} {
|
||||||
|
name := strings.Join(args, " ")
|
||||||
|
|
||||||
|
out, err := execute(t, args...)
|
||||||
|
require.ErrorIs(t, err, derive.ErrIndexTooLarge, name)
|
||||||
|
require.Empty(t, out, name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAnIndexWithALeadingZeroIsDecimal(t *testing.T) {
|
func TestAnIndexWithALeadingZeroIsDecimal(t *testing.T) {
|
||||||
@@ -181,8 +202,10 @@ func TestAChildMnemonicIsPrinted(t *testing.T) {
|
|||||||
require.NotEqual(t, short, next)
|
require.NotEqual(t, short, next)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The mnemonic command fails in this run, so the refusal of the length
|
||||||
|
// shows that it was checked before the mnemonic was looked for.
|
||||||
func TestALengthTheToolDoesNotOfferIsRefused(t *testing.T) {
|
func TestALengthTheToolDoesNotOfferIsRefused(t *testing.T) {
|
||||||
t.Setenv(mnemonic.Variable, example())
|
t.Setenv(mnemonic.CommandVariable, failingMnemonicCommand)
|
||||||
|
|
||||||
out, err := execute(t, "mnemonic", "--words", "15")
|
out, err := execute(t, "mnemonic", "--words", "15")
|
||||||
require.ErrorIs(t, err, childmnemonic.ErrWordCount)
|
require.ErrorIs(t, err, childmnemonic.ErrWordCount)
|
||||||
|
|||||||
@@ -52,6 +52,11 @@ func derived(cmd *cobra.Command) (string, error) {
|
|||||||
return "", fmt.Errorf("reading the word count: %w", err)
|
return "", fmt.Errorf("reading the word count: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = childmnemonic.CheckWords(words)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
parent, err := options.Mnemonic(cmd)
|
parent, err := options.Mnemonic(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"sneak.berlin/go/keyfunc/internal/derive"
|
||||||
"sneak.berlin/go/keyfunc/internal/mnemonic"
|
"sneak.berlin/go/keyfunc/internal/mnemonic"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -23,13 +24,20 @@ func Add(cmd *cobra.Command) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Index returns the key index the user asked for.
|
// Index returns the key index the user asked for, and refuses one above
|
||||||
|
// derive.MaxIndex. Every command calls it before it looks for the
|
||||||
|
// mnemonic.
|
||||||
func Index(cmd *cobra.Command) (uint32, error) {
|
func Index(cmd *cobra.Command) (uint32, error) {
|
||||||
index, err := cmd.Flags().GetUint32("index")
|
index, err := cmd.Flags().GetUint32("index")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("reading the index: %w", err)
|
return 0, fmt.Errorf("reading the index: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = derive.CheckIndex(index)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
return index, nil
|
return index, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user