From 45463fa35b9d163a36e47d118e50d4112230b01f Mon Sep 17 00:00:00 2001 From: clawbot Date: Wed, 7 Oct 2026 13:43:37 +0200 Subject: [PATCH 1/2] Require secret at the commit that drops go-bip39 (closes #70) sneak/secret now carries its own copy of go-bip39 (https://git.eeqj.de/sneak/secret/pulls/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 (https://git.eeqj.de/sneak/secret/issues/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 --- README.md | 2 +- go.mod | 2 +- go.sum | 6 ++---- internal/childmnemonic/childmnemonic.go | 2 +- internal/derive/derive.go | 2 +- 5 files changed, 6 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 4903928..4372b8b 100644 --- a/README.md +++ b/README.md @@ -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 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. Commands are grouped by what is derived: `keyfunc ssh ...` for ed25519 SSH keys, diff --git a/go.mod b/go.mod index 107494e..8a4b92a 100644 --- a/go.mod +++ b/go.mod @@ -4,13 +4,13 @@ go 1.26.0 require ( 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/btcutil v1.2.0 github.com/spf13/cobra v1.10.2 github.com/stretchr/testify v1.12.1 golang.org/x/crypto v0.57.0 golang.org/x/term v0.46.0 + sneak.berlin/go/secret v0.0.0-20261007105610-ef0ae90768c8 ) require ( diff --git a/go.sum b/go.sum index 5c2cbff..a6b1fa6 100644 --- a/go.sum +++ b/go.sum @@ -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/hpke v0.4.0 h1:p575VVQ6ted4pL+it6M00V/f2qTZITO0zgmdKCkd5+A= 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/go.mod h1:qbPE+pEiR9643E1s1xu57awsRhlCIm1ZIi6FfeRA4KE= 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/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= 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.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= 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/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc= 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= diff --git a/internal/childmnemonic/childmnemonic.go b/internal/childmnemonic/childmnemonic.go index c4d6b89..7b6c7d0 100644 --- a/internal/childmnemonic/childmnemonic.go +++ b/internal/childmnemonic/childmnemonic.go @@ -5,10 +5,10 @@ import ( "errors" "fmt" - "git.eeqj.de/sneak/secret/pkg/bip85" "github.com/btcsuite/btcd/btcutil/hdkeychain" "sneak.berlin/go/keyfunc/internal/bip39" "sneak.berlin/go/keyfunc/internal/derive" + "sneak.berlin/go/secret/pkg/bip85" ) // english is the number BIP-85 gives the English word list. diff --git a/internal/derive/derive.go b/internal/derive/derive.go index cc98fa3..55b944b 100644 --- a/internal/derive/derive.go +++ b/internal/derive/derive.go @@ -5,10 +5,10 @@ import ( "errors" "fmt" - "git.eeqj.de/sneak/secret/pkg/bip85" "github.com/btcsuite/btcd/btcutil/hdkeychain" "github.com/btcsuite/btcd/chaincfg" "sneak.berlin/go/keyfunc/internal/bip39" + "sneak.berlin/go/secret/pkg/bip85" ) const ( -- 2.54.0 From efca0ebb3944955ec927da57a1f23c0291d24353 Mon Sep 17 00:00:00 2001 From: clawbot <35+clawbot@noreply.example.org> Date: Tue, 13 Oct 2026 02:29:25 +0200 Subject: [PATCH 2/2] Read --index and --words as decimal numbers (closes #75) 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 --- README.md | 4 +- internal/cli/cli_test.go | 67 +++++++++++++++++++++++++++++++ internal/cli/mnemonic/mnemonic.go | 5 ++- internal/cli/options/options.go | 35 +++++++++++++++- 4 files changed, 106 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 4372b8b..57b39bd 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,9 @@ m/83696968'/'/' 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): 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 diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go index 0b29adc..8113091 100644 --- a/internal/cli/cli_test.go +++ b/internal/cli/cli_test.go @@ -2,6 +2,7 @@ package cli_test import ( "bytes" + "strconv" "strings" "testing" @@ -106,6 +107,56 @@ func TestAnIndexWithNoHardenedChildIsRefused(t *testing.T) { 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) { t.Setenv(mnemonic.Variable, "") @@ -138,6 +189,22 @@ func TestALengthTheToolDoesNotOfferIsRefused(t *testing.T) { 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 // wrote to standard output. func run(t *testing.T, args ...string) string { diff --git a/internal/cli/mnemonic/mnemonic.go b/internal/cli/mnemonic/mnemonic.go index 8c33332..05fdcea 100644 --- a/internal/cli/mnemonic/mnemonic.go +++ b/internal/cli/mnemonic/mnemonic.go @@ -31,8 +31,9 @@ func Command() *cobra.Command { }, } - cmd.Flags().Uint32( - "words", childmnemonic.DefaultWords, + words := options.Decimal(childmnemonic.DefaultWords) + cmd.Flags().Var( + &words, "words", "how long the child mnemonic is: 12, 18 or 24 words", ) diff --git a/internal/cli/options/options.go b/internal/cli/options/options.go index 5d454ec..c008541 100644 --- a/internal/cli/options/options.go +++ b/internal/cli/options/options.go @@ -4,6 +4,7 @@ package options import ( "fmt" + "strconv" "github.com/spf13/cobra" "sneak.berlin/go/keyfunc/internal/mnemonic" @@ -16,8 +17,8 @@ func Add(cmd *cobra.Command) { "mnemonic-command", "", "shell command whose output is the mnemonic", ) - cmd.PersistentFlags().Uint32P( - "index", "n", 0, + cmd.PersistentFlags().VarP( + new(Decimal), "index", "n", "which key to derive", ) } @@ -41,3 +42,33 @@ func Mnemonic(cmd *cobra.Command) (string, error) { 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" +} -- 2.54.0