2 Commits
Author SHA1 Message Date
clawbot efca0ebb39 Read --index and --words as decimal numbers (closes #75)
check / check (push) Failing after 3m40s
Both flags were uint32 flags, which the flag library reads with base-0 rules, so `-n 010` derived key 8 and `0x10` or `1_0` were accepted. Both now hold options.Decimal, a flag value whose Set parses with strconv.ParseUint(text, 10, 32): a leading zero stays decimal, and a base prefix, an underscore, a sign or an empty value is refused with an error naming the flag while the flags are parsed, before any command runs. Its type is still named uint32, so --help is unchanged and GetUint32 still reads it back. The README says the index is a decimal number.

Model: opus-5-5
2026-10-13 02:29:25 +02:00
clawbot 45463fa35b Require secret at the commit that drops go-bip39 (closes #70)
check / check (push) Successful in 5m8s
sneak/secret now carries its own copy of go-bip39
(sneak/secret#130), so requiring it at that
commit, ef0ae90, lets go mod tidy drop go-bip39 from go.sum. Earlier,
secret renamed its module to sneak.berlin/go/secret
(sneak/secret#43), so the requirement, the
two imports of its bip85 package and the README sentence naming that
package move to the new path. The bip85 functions keyfunc calls are
unchanged, and no other requirement moves.

Model: opus-5-5
Co-authored-by: clawbot <sneak+clawbot@sneak.cloud>
2026-10-07 13:43:37 +02:00
8 changed files with 112 additions and 13 deletions
+4 -2
View File
@@ -6,7 +6,7 @@ identities and child mnemonics, each of which can be recreated from that
mnemonic at any time. The same mnemonic, key type and index always give the same mnemonic at any time. The same mnemonic, key type and index always give the same
key. key.
It uses the BIP-85 entropy deriver from `git.eeqj.de/sneak/secret/pkg/bip85` and It uses the BIP-85 entropy deriver from `sneak.berlin/go/secret/pkg/bip85` and
takes the same steps as that repository's `agehd` package. takes the same steps as that repository's `agehd` package.
Commands are grouped by what is derived: `keyfunc ssh ...` for ed25519 SSH keys, Commands are grouped by what is derived: `keyfunc ssh ...` for ed25519 SSH keys,
@@ -100,7 +100,9 @@ m/83696968'/<app>'/<n>'
to one tool. Each key type's number is spelled the way BIP-85 spells its own to one tool. Each key type's number is spelled the way BIP-85 spells its own
RSA application (`828365` is the ASCII codes of `R`, `S`, `A` written out): RSA application (`828365` is the ASCII codes of `R`, `S`, `A` written out):
SSH is `838372` (`S` `S` `H`), age is `657169` (`A` `G` `E`). SSH is `838372` (`S` `S` `H`), age is `657169` (`A` `G` `E`).
- `n` is the key index: flag `--index` / `-n`, default `0`. - `n` is the key index, a decimal number: flag `--index` / `-n`, default `0`. A
leading zero is still decimal (`-n 010` is index 10); a base prefix, an
underscore or a sign (`0x10`, `1_0`, `+1`) is refused.
## Giving it the mnemonic ## Giving it the mnemonic
+1 -1
View File
@@ -4,13 +4,13 @@ go 1.26.0
require ( require (
filippo.io/age v1.3.2 filippo.io/age v1.3.2
git.eeqj.de/sneak/secret v0.0.0-20260810132333-41cea400a7fd
github.com/btcsuite/btcd v0.25.0 github.com/btcsuite/btcd v0.25.0
github.com/btcsuite/btcd/btcutil v1.2.0 github.com/btcsuite/btcd/btcutil v1.2.0
github.com/spf13/cobra v1.10.2 github.com/spf13/cobra v1.10.2
github.com/stretchr/testify v1.12.1 github.com/stretchr/testify v1.12.1
golang.org/x/crypto v0.57.0 golang.org/x/crypto v0.57.0
golang.org/x/term v0.46.0 golang.org/x/term v0.46.0
sneak.berlin/go/secret v0.0.0-20261007105610-ef0ae90768c8
) )
require ( require (
+2 -4
View File
@@ -4,8 +4,6 @@ filippo.io/age v1.3.2 h1:r6RSZLFSMm6rzKepZ7ZAYkKCu14f3/Me8c7uKYh7C8c=
filippo.io/age v1.3.2/go.mod h1:TH/Yr2sSRhCKbaH4XPxpUV0Us8Gv6txYUpiZQWz8Evk= filippo.io/age v1.3.2/go.mod h1:TH/Yr2sSRhCKbaH4XPxpUV0Us8Gv6txYUpiZQWz8Evk=
filippo.io/hpke v0.4.0 h1:p575VVQ6ted4pL+it6M00V/f2qTZITO0zgmdKCkd5+A= filippo.io/hpke v0.4.0 h1:p575VVQ6ted4pL+it6M00V/f2qTZITO0zgmdKCkd5+A=
filippo.io/hpke v0.4.0/go.mod h1:EmAN849/P3qdeK+PCMkDpDm83vRHM5cDipBJ8xbQLVY= filippo.io/hpke v0.4.0/go.mod h1:EmAN849/P3qdeK+PCMkDpDm83vRHM5cDipBJ8xbQLVY=
git.eeqj.de/sneak/secret v0.0.0-20260810132333-41cea400a7fd h1:6YFV6horz2wDFPWWhour8qx8gLGyO0qoplwEeOuQ2J4=
git.eeqj.de/sneak/secret v0.0.0-20260810132333-41cea400a7fd/go.mod h1:gKCcMZvlBOqusn/BxR8IyFmSJQr6R4vvjJ926iNpOSI=
github.com/btcsuite/btcd v0.25.0 h1:JPbjwvHGpSywBRuorFFqTjaVP4y6Qw69XJ1nQ6MyWJM= github.com/btcsuite/btcd v0.25.0 h1:JPbjwvHGpSywBRuorFFqTjaVP4y6Qw69XJ1nQ6MyWJM=
github.com/btcsuite/btcd v0.25.0/go.mod h1:qbPE+pEiR9643E1s1xu57awsRhlCIm1ZIi6FfeRA4KE= github.com/btcsuite/btcd v0.25.0/go.mod h1:qbPE+pEiR9643E1s1xu57awsRhlCIm1ZIi6FfeRA4KE=
github.com/btcsuite/btcd/btcec/v2 v2.5.0 h1:KioMXOWa76b86sTZZOmbzv/ldaQCmB8KFAyn5PbB8E8= github.com/btcsuite/btcd/btcec/v2 v2.5.0 h1:KioMXOWa76b86sTZZOmbzv/ldaQCmB8KFAyn5PbB8E8=
@@ -30,8 +28,6 @@ github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8=
github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
@@ -42,3 +38,5 @@ golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE= golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc= golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
sneak.berlin/go/secret v0.0.0-20261007105610-ef0ae90768c8 h1:Bd2bUsRCN8xhcrpH3LpJqHxR2QffeOuF5z76iP4BFnw=
sneak.berlin/go/secret v0.0.0-20261007105610-ef0ae90768c8/go.mod h1:uVx9ZuE7ZMIOBY2B1ElXCs97CTsEfuham3NQRdLj5lI=
+1 -1
View File
@@ -5,10 +5,10 @@ import (
"errors" "errors"
"fmt" "fmt"
"git.eeqj.de/sneak/secret/pkg/bip85"
"github.com/btcsuite/btcd/btcutil/hdkeychain" "github.com/btcsuite/btcd/btcutil/hdkeychain"
"sneak.berlin/go/keyfunc/internal/bip39" "sneak.berlin/go/keyfunc/internal/bip39"
"sneak.berlin/go/keyfunc/internal/derive" "sneak.berlin/go/keyfunc/internal/derive"
"sneak.berlin/go/secret/pkg/bip85"
) )
// english is the number BIP-85 gives the English word list. // english is the number BIP-85 gives the English word list.
+67
View File
@@ -2,6 +2,7 @@ package cli_test
import ( import (
"bytes" "bytes"
"strconv"
"strings" "strings"
"testing" "testing"
@@ -106,6 +107,56 @@ func TestAnIndexWithNoHardenedChildIsRefused(t *testing.T) {
require.Empty(t, out) require.Empty(t, out)
} }
func TestAnIndexWithALeadingZeroIsDecimal(t *testing.T) {
t.Setenv(mnemonic.Variable, example())
line := run(t, "ssh", "pub", "-n", "010")
require.Equal(t, run(t, "ssh", "pub", "-n", "10"), line)
require.True(t,
strings.HasSuffix(strings.TrimSpace(line), " keyfunc/ssh/10"),
)
}
// The mnemonic command in these runs fails, so a refusal naming the
// flag shows that the number was refused before the mnemonic was
// looked for.
func TestAnIndexThatIsNotDecimalDigitsIsRefused(t *testing.T) {
t.Parallel()
for _, index := range []string{"0x10", "1_0", "+1", "-1", ""} {
t.Run(index, func(t *testing.T) {
t.Parallel()
out, err := execute(t,
"ssh", "pub", "-n", index, "--mnemonic-command", "exit 1",
)
require.ErrorIs(t, err, strconv.ErrSyntax)
require.ErrorContains(t, err, "--index")
require.Empty(t, out)
})
}
}
// Neither a command group nor --version reads the index, so these
// show that it is refused while the flags are parsed.
func TestAnIndexNotInDecimalIsRefusedBeforeAnyCommandRuns(t *testing.T) {
t.Parallel()
for _, args := range [][]string{
{"ssh", "-n", "0x10"},
{"-n", "0x10", "--version"},
} {
t.Run(strings.Join(args, " "), func(t *testing.T) {
t.Parallel()
out, err := execute(t, args...)
require.ErrorIs(t, err, strconv.ErrSyntax)
require.ErrorContains(t, err, "--index")
require.Empty(t, out)
})
}
}
func TestTheMnemonicCommandIsUsed(t *testing.T) { func TestTheMnemonicCommandIsUsed(t *testing.T) {
t.Setenv(mnemonic.Variable, "") t.Setenv(mnemonic.Variable, "")
@@ -138,6 +189,22 @@ func TestALengthTheToolDoesNotOfferIsRefused(t *testing.T) {
require.Empty(t, out) require.Empty(t, out)
} }
func TestTheWordCountIsDecimal(t *testing.T) {
t.Setenv(mnemonic.Variable, example())
words := strings.Fields(run(t, "mnemonic", "--words", "012"))
require.Len(t, words, twelve)
// As for the index, the failing mnemonic command shows that the
// number was refused first.
out, err := execute(t,
"mnemonic", "--words", "0x18", "--mnemonic-command", "exit 1",
)
require.ErrorIs(t, err, strconv.ErrSyntax)
require.ErrorContains(t, err, "--words")
require.Empty(t, out)
}
// run executes the tool with the given arguments and returns what it // run executes the tool with the given arguments and returns what it
// wrote to standard output. // wrote to standard output.
func run(t *testing.T, args ...string) string { func run(t *testing.T, args ...string) string {
+3 -2
View File
@@ -31,8 +31,9 @@ func Command() *cobra.Command {
}, },
} }
cmd.Flags().Uint32( words := options.Decimal(childmnemonic.DefaultWords)
"words", childmnemonic.DefaultWords, cmd.Flags().Var(
&words, "words",
"how long the child mnemonic is: 12, 18 or 24 words", "how long the child mnemonic is: 12, 18 or 24 words",
) )
+33 -2
View File
@@ -4,6 +4,7 @@ package options
import ( import (
"fmt" "fmt"
"strconv"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"sneak.berlin/go/keyfunc/internal/mnemonic" "sneak.berlin/go/keyfunc/internal/mnemonic"
@@ -16,8 +17,8 @@ func Add(cmd *cobra.Command) {
"mnemonic-command", "", "mnemonic-command", "",
"shell command whose output is the mnemonic", "shell command whose output is the mnemonic",
) )
cmd.PersistentFlags().Uint32P( cmd.PersistentFlags().VarP(
"index", "n", 0, new(Decimal), "index", "n",
"which key to derive", "which key to derive",
) )
} }
@@ -41,3 +42,33 @@ func Mnemonic(cmd *cobra.Command) (string, error) {
return mnemonic.Read(cmd.Context(), command) return mnemonic.Read(cmd.Context(), command)
} }
// Decimal is a flag value for a number written in decimal digits only.
// The flag library's own uint32 flags read 010 as octal 8 and also
// take 0x10 or 1_0.
type Decimal uint32
// String returns the number in decimal digits.
func (d *Decimal) String() string {
return strconv.FormatUint(uint64(*d), 10)
}
// Set reads text as a decimal number that fits in 32 bits: a leading
// zero is still decimal, and a base prefix, an underscore or a sign is
// refused.
func (d *Decimal) Set(text string) error {
number, err := strconv.ParseUint(text, 10, 32)
if err != nil {
return err
}
*d = Decimal(number)
return nil
}
// Type is what --help shows as the flag's type, and what GetUint32
// checks before it reads the value back.
func (d *Decimal) Type() string {
return "uint32"
}
+1 -1
View File
@@ -5,10 +5,10 @@ import (
"errors" "errors"
"fmt" "fmt"
"git.eeqj.de/sneak/secret/pkg/bip85"
"github.com/btcsuite/btcd/btcutil/hdkeychain" "github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg"
"sneak.berlin/go/keyfunc/internal/bip39" "sneak.berlin/go/keyfunc/internal/bip39"
"sneak.berlin/go/secret/pkg/bip85"
) )
const ( const (