Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
835e6a6992 |
+5
-13
@@ -18,16 +18,9 @@
|
||||
# does not need .git/config; that file can hold a credential, such as a
|
||||
# password in a remote URL or the token the CI checkout step stores there.
|
||||
# Each submodule keeps a config with the same exposure in its git directory
|
||||
# under .git/modules/, nested again for a submodule's own submodules, or in
|
||||
# its own .git directory when it keeps one.
|
||||
# KNOWN GAP: a submodule whose name has a `config` segment (`config`,
|
||||
# `deploy/config`, `config/lib`) loses its whole git directory, because
|
||||
# `**/.git/modules/**/config` also matches that segment's directory
|
||||
# under .git/modules/. Go's version stamping then fails the build;
|
||||
# nothing leaks. Name such a submodule without that segment:
|
||||
# `git submodule add --name`.
|
||||
**/.git/config
|
||||
**/.git/modules/**/config
|
||||
# under .git/modules/, nested again for a submodule's own submodules.
|
||||
.git/config
|
||||
.git/modules/**/config
|
||||
|
||||
# Agent scratch: one full checkout of the repo per in-flight agent.
|
||||
# Anchored because it occurs once where agents run at the repo root.
|
||||
@@ -51,9 +44,7 @@
|
||||
**/[iI][dD]_[rR][sS][aA]
|
||||
**/[iI][dD]_[dD][sS][aA]
|
||||
**/[iI][dD]_[eE][cC][dD][sS][aA]
|
||||
**/[iI][dD]_[eE][cC][dD][sS][aA]_[sS][kK]
|
||||
**/[iI][dD]_[eE][dD]25519
|
||||
**/[iI][dD]_[eE][dD]25519_[sS][kK]
|
||||
|
||||
# Dependencies: restored inside the image, never copied in.
|
||||
**/node_modules
|
||||
@@ -71,5 +62,6 @@
|
||||
**/.vscode
|
||||
**/*.sublime-*
|
||||
|
||||
# The binary `make build` writes.
|
||||
# The binary `make build` writes, and the CI workflow, which is not a
|
||||
# build input.
|
||||
/keyfunc
|
||||
|
||||
@@ -42,9 +42,7 @@ node_modules/
|
||||
[iI][dD]_[rR][sS][aA]
|
||||
[iI][dD]_[dD][sS][aA]
|
||||
[iI][dD]_[eE][cC][dD][sS][aA]
|
||||
[iI][dD]_[eE][cC][dD][sS][aA]_[sS][kK]
|
||||
[iI][dD]_[eE][dD]25519
|
||||
[iI][dD]_[eE][dD]25519_[sS][kK]
|
||||
|
||||
# The binary `make build` writes.
|
||||
/keyfunc
|
||||
|
||||
@@ -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 `sneak.berlin/go/secret/pkg/bip85` and
|
||||
It uses the BIP-85 entropy deriver from `git.eeqj.de/sneak/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,
|
||||
@@ -69,8 +69,6 @@ calls into `internal/`. The packages there are:
|
||||
`cli/options` holds the flags every command shares, `cli/signals` catches
|
||||
SIGINT, SIGTERM and SIGHUP for the commands that clean up before they end, and
|
||||
`cli/ssh`, `cli/age` and `cli/mnemonic` are the command groups.
|
||||
- `internal/bip39` is a copy of `github.com/tyler-smith/go-bip39` v1.1.0,
|
||||
trimmed to what keyfunc uses.
|
||||
|
||||
### Adding a key type
|
||||
|
||||
@@ -100,9 +98,7 @@ m/83696968'/<app>'/<n>'
|
||||
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, 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.
|
||||
- `n` is the key index: flag `--index` / `-n`, default `0`.
|
||||
|
||||
## Giving it the mnemonic
|
||||
|
||||
@@ -119,22 +115,14 @@ order; the first one found wins:
|
||||
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
|
||||
refuses and exits with status 1. A mnemonic that is empty, does not have 12, 15,
|
||||
18, 21 or 24 words, has a word that is not on the BIP-39 word list, or fails the
|
||||
BIP-39 checksum is refused with a message saying which. A word that is not on
|
||||
the list is named by its position, never printed. 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.
|
||||
|
||||
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.
|
||||
refuses and exits with status 1. A mnemonic that fails the BIP-39 checksum is
|
||||
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 mnemonic command, the system `ssh` (`keyfunc ssh to`) and
|
||||
`sftp` (`keyfunc ssh install`) are started, so the mnemonic is never handed on
|
||||
to them.
|
||||
environment before the system `ssh` (`keyfunc ssh to`) and `sftp`
|
||||
(`keyfunc ssh install`) are started, so the mnemonic is never handed on to them.
|
||||
|
||||
Every command takes `--index` / `-n` and `--mnemonic-command`, and has `--help`.
|
||||
`keyfunc --version` prints the version. `make build` stamps it; a binary
|
||||
@@ -227,15 +215,11 @@ first, or pass `-o StrictHostKeyChecking=accept-new` after `--`.
|
||||
|
||||
Derives the key, serves it from an SSH agent that runs inside the tool on a unix
|
||||
socket in a new private `0700` temporary directory, then runs the system `ssh`
|
||||
with `-o IdentityAgent=<that socket> -o IdentitiesOnly=no` followed by the host
|
||||
and all remaining arguments unchanged. Without `IdentitiesOnly=no`, an
|
||||
`IdentitiesOnly yes` in the user's `ssh` configuration keeps the derived key
|
||||
from being offered. The agent holds no other key, so turning it off adds nothing
|
||||
else to what `ssh` offers. The tool exits with `ssh`'s exit status and removes
|
||||
the socket and directory on the way out. The private key is never written to
|
||||
disk. A SIGINT, SIGTERM or SIGHUP ends `ssh` and still removes the socket and
|
||||
directory, and the tool then exits with status 1 unless `ssh` reported one of
|
||||
its own.
|
||||
with `-o IdentityAgent=<that socket>` followed by the host and all remaining
|
||||
arguments unchanged. The tool exits with `ssh`'s exit status and removes the
|
||||
socket and directory on the way out. The private key is never written to disk. A
|
||||
SIGINT, SIGTERM or SIGHUP ends `ssh` and still removes the socket and directory,
|
||||
and the tool then exits with status 1 unless `ssh` reported one of its own.
|
||||
|
||||
## age identities: `keyfunc age`
|
||||
|
||||
@@ -389,7 +373,9 @@ standard: most Makefile targets are thin shims over an executable in `script/`
|
||||
|
||||
## TODO
|
||||
|
||||
No issues are open.
|
||||
The open issues that stand between the tree and a 1.0 release:
|
||||
|
||||
- [#42 go-bip39 no longer exists upstream: keep it, or copy it into the repo?](https://git.eeqj.de/sneak/keyfunc/issues/42)
|
||||
|
||||
## License
|
||||
|
||||
|
||||
+12
-44
@@ -104,14 +104,10 @@ style conventions are in separate documents:
|
||||
`lint` phase and a `test` phase, with the final stage depending on both so the
|
||||
image cannot be built unless they pass. For non-server repos the final stage
|
||||
brings up a development environment; for server repos it is the runtime image.
|
||||
The gate phases and the build stage start from their pinned base images and
|
||||
install what those images lack either inline, as the canonical Go `Dockerfile`
|
||||
below does for `git`, or by running `script/bootstrap`, as the `prompts`
|
||||
repo's own `Dockerfile` does for its yarn packages. The development
|
||||
environment stage installs development prerequisites by running
|
||||
`script/bootstrap` rather than duplicating its installs inline. A stage that
|
||||
runs `script/bootstrap` COPYs `script/` and the dependency manifests
|
||||
(`package.json` + `yarn.lock`, `go.mod` + `go.sum`, etc.) before running it.
|
||||
Dockerfiles install development prerequisites by running `script/bootstrap`
|
||||
rather than duplicating installs inline; COPY `script/` and the dependency
|
||||
manifests (`package.json` + `yarn.lock`, `go.mod` + `go.sum`, etc.) before
|
||||
running it.
|
||||
|
||||
- **Linting and testing run in Docker, as phases of the `Dockerfile`.** There is
|
||||
no separate lint file. `script/lint` and `script/test` each build one phase
|
||||
@@ -160,9 +156,6 @@ style conventions are in separate documents:
|
||||
not evidence that anything ran: a sub-second build reporting success is a
|
||||
cache hit, not a result. Never invalidate by pruning — `docker builder prune`
|
||||
and friends destroy a build cache shared with every other build on the host.
|
||||
When a check is added or changed, prove it works by planting a defect it must
|
||||
catch and watching the run fail on it, then revert the defect. A green run
|
||||
alone shows neither that the check ran nor that it covers what it should.
|
||||
|
||||
- **The gate phases are separate stages, and the build stage depends on both.**
|
||||
The lint phase is based on the `golangci/golangci-lint` image (pinned by
|
||||
@@ -243,28 +236,13 @@ style conventions are in separate documents:
|
||||
(e.g. a web frontend compiled in a separate stage), the lint phase must
|
||||
create placeholder files so the embed directives resolve. Example:
|
||||
`RUN mkdir -p web/dist && touch web/dist/index.html web/dist/style.css`.
|
||||
- If the project requires CGO or system libraries for linting, install them
|
||||
in the lint phase. The `golangci/golangci-lint` image is Debian-based and
|
||||
has no `apk`, so install with `apt-get` under the Debian package name
|
||||
(`libvips-dev`, where alpine says `vips-dev`), and delete the package
|
||||
lists in the same `RUN`, so the layer does not keep them:
|
||||
|
||||
```dockerfile
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends libvips-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
```
|
||||
|
||||
- `.dockerignore` lets `.git` into the build context. It keeps out every git
|
||||
`config` at any depth (`**/.git/config`, `**/.git/modules/**/config`): the
|
||||
repository's own, each submodule's under `.git/modules/`, and that of a
|
||||
submodule keeping its own `.git` directory. `git describe` does not need
|
||||
them, and each can hold a credential: a password in a remote URL, or the
|
||||
token the CI checkout step stores there. A submodule whose name has a
|
||||
`config` segment (`config`, `deploy/config`, `config/lib`) loses its whole
|
||||
git directory to `**/.git/modules/**/config`, and Go's version stamping
|
||||
then fails the build: give it a name without that segment
|
||||
(`git submodule add --name`). The stage that compiles has `git` (the
|
||||
- If the project requires CGO or system libraries for linting (e.g.
|
||||
`vips-dev`), install them in the lint phase with `apk add`.
|
||||
- `.dockerignore` lets `.git` into the build context. It keeps out
|
||||
`.git/config` and each submodule's `config` under `.git/modules/` at any
|
||||
depth (`.git/modules/**/config`), which `git describe` does not need and
|
||||
which can hold a credential: a password in a remote URL, or the token the
|
||||
CI checkout step stores there. The stage that compiles has `git` (the
|
||||
Debian Go image has it; an alpine one needs `apk add --no-cache git`) and
|
||||
takes the version from the `VERSION` build argument when one is given,
|
||||
otherwise from `git describe --tags --always`. That gives the tag on a
|
||||
@@ -286,12 +264,7 @@ style conventions are in separate documents:
|
||||
carry the same guarantee, because its gate phases may come from the cache. The
|
||||
image build is uncached and so runs the gate phases a second time. That is the
|
||||
price of the rule above, and it is worth paying: the image that ships is built
|
||||
from a run of its own gates rather than from a cache entry. A separate
|
||||
workflow limited to `main` by a `branches` list under `on: push` cannot be
|
||||
checked by review: to try a change to it, add the feature branch to that list
|
||||
and push, then remove the branch from the list again before merging. Keep any
|
||||
job in it that publishes behind `if: github.ref_name == 'main'`, so the run
|
||||
from the feature branch publishes nothing.
|
||||
from a run of its own gates rather than from a cache entry.
|
||||
|
||||
- Use platform-standard formatters: `black` for Python, `prettier` for
|
||||
JS/CSS/Markdown/HTML, `go fmt` for Go. Always use default configuration with
|
||||
@@ -522,11 +495,6 @@ style conventions are in separate documents:
|
||||
|
||||
Keep it POSIX sh: no arrays, no `[[`, no `grep -P`.
|
||||
|
||||
A Go tool a repo needs on the host is installed with `go install` pinned to
|
||||
a commit hash (`go install <package>@<commit hash>`). It is never tracked as
|
||||
a `go.mod` tool dependency or through a `tools.go` file, either of which
|
||||
pulls the tool's own dependencies into the repo's `go.mod` and `go.sum`.
|
||||
|
||||
- When pinning images or packages by hash, add a comment above the reference
|
||||
with the version and date (YYYY-MM-DD).
|
||||
|
||||
|
||||
@@ -4,13 +4,14 @@ 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
|
||||
github.com/tyler-smith/go-bip39 v1.1.0
|
||||
golang.org/x/crypto v0.57.0
|
||||
golang.org/x/term v0.46.0
|
||||
sneak.berlin/go/secret v0.0.0-20261007105610-ef0ae90768c8
|
||||
)
|
||||
|
||||
require (
|
||||
|
||||
@@ -4,6 +4,8 @@ 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=
|
||||
@@ -28,15 +30,21 @@ 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=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
|
||||
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||
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=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
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=
|
||||
|
||||
@@ -94,12 +94,15 @@ func (k *Key) Identity() string {
|
||||
}
|
||||
|
||||
// Encrypt copies src to dst, encrypted to this key and to every extra
|
||||
// recipient given, so the same mnemonic can always read it back again.
|
||||
// recipient named, so the same mnemonic can always read it back again.
|
||||
// Armored output is the text form age also reads.
|
||||
func (k *Key) Encrypt(
|
||||
dst io.Writer, src io.Reader, to []age.Recipient, armored bool,
|
||||
dst io.Writer, src io.Reader, to []string, armored bool,
|
||||
) error {
|
||||
all := append([]age.Recipient{k.identity.Recipient()}, to...)
|
||||
all, err := recipients(k.identity.Recipient(), to)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
out := dst
|
||||
|
||||
@@ -110,7 +113,7 @@ func (k *Key) Encrypt(
|
||||
out = text
|
||||
}
|
||||
|
||||
err := encrypt(out, src, all)
|
||||
err = encrypt(out, src, all)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -127,12 +130,12 @@ func (k *Key) Encrypt(
|
||||
return nil
|
||||
}
|
||||
|
||||
// Recipients reads the recipients named on the command line, for
|
||||
// Encrypt to add to the key's own.
|
||||
func Recipients(names []string) ([]age.Recipient, error) {
|
||||
list := make([]age.Recipient, 0, len(names))
|
||||
// recipients returns the key's own recipient followed by the ones
|
||||
// named on the command line, so what the key encrypts it can read.
|
||||
func recipients(mine age.Recipient, to []string) ([]age.Recipient, error) {
|
||||
list := []age.Recipient{mine}
|
||||
|
||||
for _, name := range names {
|
||||
for _, name := range to {
|
||||
parsed, err := age.ParseX25519Recipient(name)
|
||||
if err != nil {
|
||||
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)
|
||||
|
||||
to, err := agekey.Recipients([]string{theirs.Recipient()})
|
||||
require.NoError(t, err)
|
||||
|
||||
var sealed bytes.Buffer
|
||||
|
||||
err = mine.Encrypt(&sealed, strings.NewReader("hello"), to, false)
|
||||
err := mine.Encrypt(
|
||||
&sealed, strings.NewReader("hello"),
|
||||
[]string{theirs.Recipient()}, false,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, key := range []*agekey.Key{mine, theirs} {
|
||||
@@ -138,7 +138,10 @@ func TestAnExtraRecipientCanReadItTooAndSoCanTheDerivedOne(t *testing.T) {
|
||||
func TestARecipientThatIsNotOneIsRefused(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := agekey.Recipients([]string{"not a recipient"})
|
||||
err := forIndex(t, 0).Encrypt(
|
||||
&bytes.Buffer{}, strings.NewReader("hello"),
|
||||
[]string{"not a recipient"}, false,
|
||||
)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014-2018 Tyler Smith and contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,268 +0,0 @@
|
||||
// Package bip39 is the Golang implementation of the BIP39 spec.
|
||||
//
|
||||
// The official BIP39 spec can be found at
|
||||
// https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki
|
||||
//
|
||||
// It is a copy of github.com/tyler-smith/go-bip39 v1.1.0, trimmed to what
|
||||
// keyfunc uses.
|
||||
//
|
||||
//nolint:mnd // the numbers are BIP-39's own, written as upstream writes them
|
||||
package bip39
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrInvalidMnemonic is returned when trying to use a malformed mnemonic.
|
||||
ErrInvalidMnemonic = errors.New("invalid mnenomic")
|
||||
|
||||
// ErrEntropyLengthInvalid is returned when trying to use an entropy set with
|
||||
// an invalid size.
|
||||
ErrEntropyLengthInvalid = errors.New(
|
||||
"entropy length must be [128, 256] and a multiple of 32",
|
||||
)
|
||||
|
||||
// ErrChecksumIncorrect is returned when entropy has the incorrect checksum.
|
||||
ErrChecksumIncorrect = errors.New("checksum incorrect")
|
||||
)
|
||||
|
||||
// EntropyFromMnemonic takes a mnemonic generated by this library,
|
||||
// and returns the input entropy used to generate the given mnemonic.
|
||||
// An error is returned if the given mnemonic is invalid.
|
||||
func EntropyFromMnemonic(mnemonic string) ([]byte, error) {
|
||||
mnemonicSlice, isValid := splitMnemonicWords(mnemonic)
|
||||
if !isValid {
|
||||
return nil, ErrInvalidMnemonic
|
||||
}
|
||||
|
||||
// Some bitwise operands for working with big.Ints
|
||||
shift11BitsMask := big.NewInt(2048)
|
||||
bigOne := big.NewInt(1)
|
||||
|
||||
// used to isolate the checksum bits from the entropy+checksum byte array
|
||||
wordLengthChecksumMasksMapping := map[int]*big.Int{
|
||||
12: big.NewInt(15),
|
||||
15: big.NewInt(31),
|
||||
18: big.NewInt(63),
|
||||
21: big.NewInt(127),
|
||||
24: big.NewInt(255),
|
||||
}
|
||||
// used to use only the desired x of 8 available checksum bits.
|
||||
// 256 bit (word length 24) requires all 8 bits of the checksum,
|
||||
// and thus no shifting is needed for it (we would get a divByZero crash if we did)
|
||||
wordLengthChecksumShiftMapping := map[int]*big.Int{
|
||||
12: big.NewInt(16),
|
||||
15: big.NewInt(8),
|
||||
18: big.NewInt(4),
|
||||
21: big.NewInt(2),
|
||||
}
|
||||
|
||||
// wordMap is a reverse lookup map for the word list
|
||||
wordMap := map[string]int{}
|
||||
for i, v := range English() {
|
||||
wordMap[v] = i
|
||||
}
|
||||
|
||||
// Decode the words into a big.Int.
|
||||
b := big.NewInt(0)
|
||||
|
||||
for _, v := range mnemonicSlice {
|
||||
index, found := wordMap[v]
|
||||
if !found {
|
||||
return nil, fmt.Errorf(
|
||||
"%w: word `%v` not found in reverse map", ErrInvalidMnemonic, v,
|
||||
)
|
||||
}
|
||||
|
||||
var wordBytes [2]byte
|
||||
|
||||
//nolint:gosec // the index of a word in the list is below 2048
|
||||
binary.BigEndian.PutUint16(wordBytes[:], uint16(index))
|
||||
|
||||
b = b.Mul(b, shift11BitsMask)
|
||||
b = b.Or(b, big.NewInt(0).SetBytes(wordBytes[:]))
|
||||
}
|
||||
|
||||
// Build and add the checksum to the big.Int.
|
||||
checksum := big.NewInt(0)
|
||||
checksumMask := wordLengthChecksumMasksMapping[len(mnemonicSlice)]
|
||||
checksum = checksum.And(b, checksumMask)
|
||||
|
||||
b.Div(b, big.NewInt(0).Add(checksumMask, bigOne))
|
||||
|
||||
// The entropy is the underlying bytes of the big.Int. Any upper bytes of
|
||||
// all 0's are not returned so we pad the beginning of the slice with empty
|
||||
// bytes if necessary.
|
||||
entropy := b.Bytes()
|
||||
entropy = padByteSlice(entropy, len(mnemonicSlice)/3*4)
|
||||
|
||||
// Generate the checksum and compare with the one we got from the mneomnic.
|
||||
entropyChecksumBytes := computeChecksum(entropy)
|
||||
entropyChecksum := big.NewInt(int64(entropyChecksumBytes[0]))
|
||||
|
||||
if l := len(mnemonicSlice); l != 24 {
|
||||
checksumShift := wordLengthChecksumShiftMapping[l]
|
||||
entropyChecksum.Div(entropyChecksum, checksumShift)
|
||||
}
|
||||
|
||||
if checksum.Cmp(entropyChecksum) != 0 {
|
||||
return nil, ErrChecksumIncorrect
|
||||
}
|
||||
|
||||
return entropy, nil
|
||||
}
|
||||
|
||||
// NewMnemonic will return a string consisting of the mnemonic words for
|
||||
// the given entropy.
|
||||
// If the provide entropy is invalid, an error will be returned.
|
||||
func NewMnemonic(entropy []byte) (string, error) {
|
||||
// Compute some lengths for convenience.
|
||||
entropyBitLength := len(entropy) * 8
|
||||
checksumBitLength := entropyBitLength / 32
|
||||
sentenceLength := (entropyBitLength + checksumBitLength) / 11
|
||||
|
||||
// Validate that the requested size is supported.
|
||||
err := validateEntropyBitSize(entropyBitLength)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Some bitwise operands for working with big.Ints
|
||||
last11BitsMask := big.NewInt(2047)
|
||||
shift11BitsMask := big.NewInt(2048)
|
||||
|
||||
// wordList is the set of words to use
|
||||
wordList := English()
|
||||
|
||||
// Add checksum to entropy.
|
||||
entropy = addChecksum(entropy)
|
||||
|
||||
// Break entropy up into sentenceLength chunks of 11 bits.
|
||||
// For each word AND mask the rightmost 11 bits and find the word at that index.
|
||||
// Then bitshift entropy 11 bits right and repeat.
|
||||
// Add to the last empty slot so we can work with LSBs instead of MSB.
|
||||
|
||||
// Entropy as an int so we can bitmask without worrying about bytes slices.
|
||||
entropyInt := new(big.Int).SetBytes(entropy)
|
||||
|
||||
// Slice to hold words in.
|
||||
words := make([]string, sentenceLength)
|
||||
|
||||
// Throw away big.Int for AND masking.
|
||||
word := big.NewInt(0)
|
||||
|
||||
for i := sentenceLength - 1; i >= 0; i-- {
|
||||
// Get 11 right most bits and bitshift 11 to the right for next time.
|
||||
word.And(entropyInt, last11BitsMask)
|
||||
entropyInt.Div(entropyInt, shift11BitsMask)
|
||||
|
||||
// Get the bytes representing the 11 bits as a 2 byte slice.
|
||||
wordBytes := padByteSlice(word.Bytes(), 2)
|
||||
|
||||
// Convert bytes to an index and add that word to the list.
|
||||
words[i] = wordList[binary.BigEndian.Uint16(wordBytes)]
|
||||
}
|
||||
|
||||
return strings.Join(words, " "), nil
|
||||
}
|
||||
|
||||
// NewSeed creates a hashed seed output given a provided string and password.
|
||||
// No checking is performed to validate that the string provided is a valid mnemonic.
|
||||
func NewSeed(mnemonic string, password string) []byte {
|
||||
return pbkdf2.Key([]byte(mnemonic), []byte("mnemonic"+password), 2048, 64, sha512.New)
|
||||
}
|
||||
|
||||
// IsMnemonicValid attempts to verify that the provided mnemonic is valid.
|
||||
// Validity is determined by both the number of words being appropriate,
|
||||
// and that all the words in the mnemonic are present in the word list.
|
||||
func IsMnemonicValid(mnemonic string) bool {
|
||||
_, err := EntropyFromMnemonic(mnemonic)
|
||||
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// Appends to data the first (len(data) / 32)bits of the result of sha256(data)
|
||||
// Currently only supports data up to 32 bytes
|
||||
func addChecksum(data []byte) []byte {
|
||||
// Some bitwise operands for working with big.Ints
|
||||
bigOne := big.NewInt(1)
|
||||
bigTwo := big.NewInt(2)
|
||||
|
||||
// Get first byte of sha256
|
||||
hash := computeChecksum(data)
|
||||
firstChecksumByte := hash[0]
|
||||
|
||||
// len() is in bytes so we divide by 4
|
||||
checksumBitLength := uint(len(data) / 4)
|
||||
|
||||
// For each bit of check sum we want we shift the data one the left
|
||||
// and then set the (new) right most bit equal to checksum bit at that index
|
||||
// staring from the left
|
||||
dataBigInt := new(big.Int).SetBytes(data)
|
||||
for i := range checksumBitLength {
|
||||
// Bitshift 1 left
|
||||
dataBigInt.Mul(dataBigInt, bigTwo)
|
||||
|
||||
// Set rightmost bit if leftmost checksum bit is set
|
||||
if firstChecksumByte&(1<<(7-i)) > 0 {
|
||||
dataBigInt.Or(dataBigInt, bigOne)
|
||||
}
|
||||
}
|
||||
|
||||
return dataBigInt.Bytes()
|
||||
}
|
||||
|
||||
func computeChecksum(data []byte) []byte {
|
||||
hasher := sha256.New()
|
||||
hasher.Write(data)
|
||||
|
||||
return hasher.Sum(nil)
|
||||
}
|
||||
|
||||
// validateEntropyBitSize ensures that entropy is the correct size for being a
|
||||
// mnemonic.
|
||||
func validateEntropyBitSize(bitSize int) error {
|
||||
if (bitSize%32) != 0 || bitSize < 128 || bitSize > 256 {
|
||||
return ErrEntropyLengthInvalid
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// padByteSlice returns a byte slice of the given size with contents of the
|
||||
// given slice left padded and any empty spaces filled with 0's.
|
||||
func padByteSlice(slice []byte, length int) []byte {
|
||||
offset := length - len(slice)
|
||||
if offset <= 0 {
|
||||
return slice
|
||||
}
|
||||
|
||||
newSlice := make([]byte, length)
|
||||
copy(newSlice[offset:], slice)
|
||||
|
||||
return newSlice
|
||||
}
|
||||
|
||||
func splitMnemonicWords(mnemonic string) ([]string, bool) {
|
||||
// Create a list of all the words in the mnemonic sentence
|
||||
words := strings.Fields(mnemonic)
|
||||
|
||||
// Get num of words
|
||||
numOfWords := len(words)
|
||||
|
||||
// The number of words should be 12, 15, 18, 21 or 24
|
||||
if numOfWords%3 != 0 || numOfWords < 12 || numOfWords > 24 {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return words, true
|
||||
}
|
||||
@@ -1,442 +0,0 @@
|
||||
package bip39
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type vector struct {
|
||||
entropy string
|
||||
mnemonic string
|
||||
seed string
|
||||
}
|
||||
|
||||
func TestNewMnemonic(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, vector := range testVectors() {
|
||||
entropy, err := hex.DecodeString(vector.entropy)
|
||||
assertNil(t, err)
|
||||
|
||||
mnemonic, err := NewMnemonic(entropy)
|
||||
assertNil(t, err)
|
||||
assertEqualString(t, vector.mnemonic, mnemonic)
|
||||
|
||||
seed := NewSeed(mnemonic, "TREZOR")
|
||||
assertEqualString(t, vector.seed, hex.EncodeToString(seed))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewMnemonicInvalidEntropy(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := NewMnemonic([]byte{})
|
||||
assertNotNil(t, err)
|
||||
}
|
||||
|
||||
func TestIsMnemonicValid(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, vector := range badMnemonicSentences() {
|
||||
assertFalse(t, IsMnemonicValid(vector.mnemonic))
|
||||
}
|
||||
|
||||
for _, vector := range testVectors() {
|
||||
assertTrue(t, IsMnemonicValid(vector.mnemonic))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPadByteSlice(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assertEqualByteSlices(t, []byte{0}, padByteSlice([]byte{}, 1))
|
||||
assertEqualByteSlices(t, []byte{0, 1}, padByteSlice([]byte{1}, 2))
|
||||
assertEqualByteSlices(t, []byte{1, 1}, padByteSlice([]byte{1, 1}, 2))
|
||||
assertEqualByteSlices(t, []byte{1, 1, 1}, padByteSlice([]byte{1, 1, 1}, 2))
|
||||
}
|
||||
|
||||
//nolint:funlen // the test vectors, kept as upstream wrote them
|
||||
func TestMnemonicToByteArrayForZeroLeadingSeeds(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ms := []string{
|
||||
"00000000000000000000000000000000",
|
||||
"00a84c51041d49acca66e6160c1fa999",
|
||||
"00ca45df1673c76537a2020bfed1dafd",
|
||||
"0019d5871c7b81fd83d474ef1c1e1dae",
|
||||
"00dcb021afb35ffcdd1d032d2056fc86",
|
||||
"0062be7bd09a27288b6cf0eb565ec739",
|
||||
"00dc705b5efa0adf25b9734226ba60d4",
|
||||
"0017747418d54c6003fa64fade83374b",
|
||||
"000d44d3ee7c3dfa45e608c65384431b",
|
||||
"008241c1ef976b0323061affe5bf24b9",
|
||||
"00a6aec77e4d16bea80b50a34991aaba",
|
||||
"0011527b8c6ddecb9d0c20beccdeb58d",
|
||||
"001c938c503c8f5a2bba2248ff621546",
|
||||
"0002f90aaf7a8327698f0031b6317c36",
|
||||
"00bff43071ed7e07f77b14f615993bac",
|
||||
"00da143e00ef17fc63b6fb22dcc2c326",
|
||||
"00ffc6764fb32a354cab1a3ddefb015d",
|
||||
"0062ef47e0985e8953f24760b7598cdd",
|
||||
"003bf9765064f71d304908d906c065f5",
|
||||
"00993851503471439d154b3613947474",
|
||||
"007ad0ffe9eae753a483a76af06dfa67",
|
||||
"00091824db9ec19e663bee51d64c83cc",
|
||||
"00f48ac621f7e3cb39b2012ac3121543",
|
||||
"0072917415cdca24dfa66c4a92c885b4",
|
||||
"0027ced2b279ea8a91d29364487cdbf4",
|
||||
"00b9c0d37fb10ba272e55842ad812583",
|
||||
"004b3d0d2b9285946c687a5350479c8c",
|
||||
"00c7c12a37d3a7f8c1532b17c89b724c",
|
||||
"00f400c5545f06ae17ad00f3041e4e26",
|
||||
"001e290be10df4d209f247ac5878662b",
|
||||
"00bf0f74568e582a7dd1ee64f792ec8b",
|
||||
"00d2e43ecde6b72b847db1539ed89e23",
|
||||
"00cecba6678505bb7bfec8ed307251f6",
|
||||
"000aeed1a9edcbb4bc88f610d3ce84eb",
|
||||
"00d06206aadfc25c2b21805d283f15ae",
|
||||
"00a31789a2ab2d54f8fadd5331010287",
|
||||
"003493c5f520e8d5c0483e895a121dc9",
|
||||
"004706112800b76001ece2e268bc830e",
|
||||
"00ab31e28bb5305be56e38337dbfa486",
|
||||
"006872fe85df6b0fa945248e6f9379d1",
|
||||
"00717e5e375da6934e3cfdf57edaf3bd",
|
||||
"007f1b46e7b9c4c76e77c434b9bccd6b",
|
||||
"00dc93735aa35def3b9a2ff676560205",
|
||||
"002cd5dcd881a49c7b87714c6a570a76",
|
||||
"0013b5af9e13fac87e0c505686cfb6bf",
|
||||
"007ab1ec9526b0bc04b64ae65fd42631",
|
||||
"00abb4e11d8385c1cca905a6a65e9144",
|
||||
"00574fc62a0501ad8afada2e246708c3",
|
||||
"005207e0a815bb2da6b4c35ec1f2bf52",
|
||||
"00f3460f136fb9700080099cbd62bc18",
|
||||
"007a591f204c03ca7b93981237112526",
|
||||
"00cfe0befd428f8e5f83a5bfc801472e",
|
||||
"00987551ac7a879bf0c09b8bc474d9af",
|
||||
"00cadd3ce3d78e49fbc933a85682df3f",
|
||||
"00bfbf2e346c855ccc360d03281455a1",
|
||||
"004cdf55d429d028f715544ce22d4f31",
|
||||
"0075c84a7d15e0ac85e1e41025eed23b",
|
||||
"00807dddd61f71725d336cab844d2cb5",
|
||||
"00422f21b77fe20e367467ed98c18410",
|
||||
"00b44d0ac622907119c626c850a462fd",
|
||||
"00363f5e7f22fc49f3cd662a28956563",
|
||||
"000fe5837e68397bbf58db9f221bdc4e",
|
||||
"0056af33835c888ef0c22599686445d3",
|
||||
"00790a8647fd3dfb38b7e2b6f578f2c6",
|
||||
"00da8d9009675cb7beec930e263014fb",
|
||||
"00d4b384540a5bb54aa760edaa4fb2fe",
|
||||
"00be9b1479ed680fdd5d91a41eb926d0",
|
||||
"009182347502af97077c40a6e74b4b5c",
|
||||
"00f5c90ee1c67fa77fd821f8e9fab4f1",
|
||||
"005568f9a2dd6b0c0cc2f5ba3d9cac38",
|
||||
"008b481f8678577d9cf6aa3f6cd6056b",
|
||||
"00c4323ece5e4fe3b6cd4c5c932931af",
|
||||
"009791f7550c3798c5a214cb2d0ea773",
|
||||
"008a7baab22481f0ad8167dd9f90d55c",
|
||||
"00f0e601519aafdc8ff94975e64c946d",
|
||||
"0083b61e0daa9219df59d697c270cd31",
|
||||
}
|
||||
|
||||
for _, m := range ms {
|
||||
seed, _ := hex.DecodeString(m)
|
||||
|
||||
mnemonic, err := NewMnemonic(seed)
|
||||
if err != nil {
|
||||
t.Errorf("%v", err)
|
||||
}
|
||||
|
||||
_, err = EntropyFromMnemonic(mnemonic)
|
||||
if err != nil {
|
||||
t.Errorf("Failed for %x - %v", seed, mnemonic)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEntropyFromMnemonic128(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testEntropyFromMnemonic(t, 128)
|
||||
}
|
||||
|
||||
func TestEntropyFromMnemonic160(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testEntropyFromMnemonic(t, 160)
|
||||
}
|
||||
|
||||
func TestEntropyFromMnemonic192(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testEntropyFromMnemonic(t, 192)
|
||||
}
|
||||
|
||||
func TestEntropyFromMnemonic224(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testEntropyFromMnemonic(t, 224)
|
||||
}
|
||||
|
||||
func TestEntropyFromMnemonic256(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testEntropyFromMnemonic(t, 256)
|
||||
}
|
||||
|
||||
//nolint:dupword,lll // the test vector, kept as upstream wrote it
|
||||
func TestEntropyFromMnemonicInvalidChecksum(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := EntropyFromMnemonic("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon yellow")
|
||||
assertEqual(t, ErrChecksumIncorrect, err)
|
||||
}
|
||||
|
||||
//nolint:dupword // the test vectors, kept as upstream wrote them
|
||||
func TestEntropyFromMnemonicInvalidMnemonicSize(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, mnemonic := range []string{
|
||||
"a a a a a a a a a a a a a a a a a a a a a a a a a", // Too many words
|
||||
"a", // Too few
|
||||
"a a a a a a a a a a a a a a", // Not multiple of 3
|
||||
} {
|
||||
_, err := EntropyFromMnemonic(mnemonic)
|
||||
assertEqual(t, ErrInvalidMnemonic, err)
|
||||
}
|
||||
}
|
||||
|
||||
func testEntropyFromMnemonic(t *testing.T, bitSize int) {
|
||||
t.Helper()
|
||||
|
||||
for range 512 {
|
||||
expectedEntropy := make([]byte, bitSize/8)
|
||||
_, err := rand.Read(expectedEntropy)
|
||||
assertNil(t, err)
|
||||
assertTrue(t, len(expectedEntropy) != 0)
|
||||
|
||||
mnemonic, err := NewMnemonic(expectedEntropy)
|
||||
assertNil(t, err)
|
||||
assertTrue(t, len(mnemonic) != 0)
|
||||
|
||||
actualEntropy, err := EntropyFromMnemonic(mnemonic)
|
||||
assertNil(t, err)
|
||||
assertEqualByteSlices(t, expectedEntropy, actualEntropy)
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:dupword,funlen,lll // the BIP-39 test vectors, kept as upstream wrote them
|
||||
func testVectors() []vector {
|
||||
return []vector{
|
||||
{
|
||||
entropy: "00000000000000000000000000000000",
|
||||
mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
|
||||
seed: "c55257c360c07c72029aebc1b53c05ed0362ada38ead3e3e9efa3708e53495531f09a6987599d18264c1e1c92f2cf141630c7a3c4ab7c81b2f001698e7463b04",
|
||||
},
|
||||
{
|
||||
entropy: "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f",
|
||||
mnemonic: "legal winner thank year wave sausage worth useful legal winner thank yellow",
|
||||
seed: "2e8905819b8723fe2c1d161860e5ee1830318dbf49a83bd451cfb8440c28bd6fa457fe1296106559a3c80937a1c1069be3a3a5bd381ee6260e8d9739fce1f607",
|
||||
},
|
||||
{
|
||||
entropy: "80808080808080808080808080808080",
|
||||
mnemonic: "letter advice cage absurd amount doctor acoustic avoid letter advice cage above",
|
||||
seed: "d71de856f81a8acc65e6fc851a38d4d7ec216fd0796d0a6827a3ad6ed5511a30fa280f12eb2e47ed2ac03b5c462a0358d18d69fe4f985ec81778c1b370b652a8",
|
||||
},
|
||||
{
|
||||
entropy: "ffffffffffffffffffffffffffffffff",
|
||||
mnemonic: "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong",
|
||||
seed: "ac27495480225222079d7be181583751e86f571027b0497b5b5d11218e0a8a13332572917f0f8e5a589620c6f15b11c61dee327651a14c34e18231052e48c069",
|
||||
},
|
||||
{
|
||||
entropy: "000000000000000000000000000000000000000000000000",
|
||||
mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon agent",
|
||||
seed: "035895f2f481b1b0f01fcf8c289c794660b289981a78f8106447707fdd9666ca06da5a9a565181599b79f53b844d8a71dd9f439c52a3d7b3e8a79c906ac845fa",
|
||||
},
|
||||
{
|
||||
entropy: "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f",
|
||||
mnemonic: "legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal will",
|
||||
seed: "f2b94508732bcbacbcc020faefecfc89feafa6649a5491b8c952cede496c214a0c7b3c392d168748f2d4a612bada0753b52a1c7ac53c1e93abd5c6320b9e95dd",
|
||||
},
|
||||
{
|
||||
entropy: "808080808080808080808080808080808080808080808080",
|
||||
mnemonic: "letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic avoid letter always",
|
||||
seed: "107d7c02a5aa6f38c58083ff74f04c607c2d2c0ecc55501dadd72d025b751bc27fe913ffb796f841c49b1d33b610cf0e91d3aa239027f5e99fe4ce9e5088cd65",
|
||||
},
|
||||
{
|
||||
entropy: "ffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||
mnemonic: "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo when",
|
||||
seed: "0cd6e5d827bb62eb8fc1e262254223817fd068a74b5b449cc2f667c3f1f985a76379b43348d952e2265b4cd129090758b3e3c2c49103b5051aac2eaeb890a528",
|
||||
},
|
||||
{
|
||||
entropy: "0000000000000000000000000000000000000000000000000000000000000000",
|
||||
mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art",
|
||||
seed: "bda85446c68413707090a52022edd26a1c9462295029f2e60cd7c4f2bbd3097170af7a4d73245cafa9c3cca8d561a7c3de6f5d4a10be8ed2a5e608d68f92fcc8",
|
||||
},
|
||||
{
|
||||
entropy: "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f",
|
||||
mnemonic: "legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth title",
|
||||
seed: "bc09fca1804f7e69da93c2f2028eb238c227f2e9dda30cd63699232578480a4021b146ad717fbb7e451ce9eb835f43620bf5c514db0f8add49f5d121449d3e87",
|
||||
},
|
||||
{
|
||||
entropy: "8080808080808080808080808080808080808080808080808080808080808080",
|
||||
mnemonic: "letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic bless",
|
||||
seed: "c0c519bd0e91a2ed54357d9d1ebef6f5af218a153624cf4f2da911a0ed8f7a09e2ef61af0aca007096df430022f7a2b6fb91661a9589097069720d015e4e982f",
|
||||
},
|
||||
{
|
||||
entropy: "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||
mnemonic: "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo vote",
|
||||
seed: "dd48c104698c30cfe2b6142103248622fb7bb0ff692eebb00089b32d22484e1613912f0a5b694407be899ffd31ed3992c456cdf60f5d4564b8ba3f05a69890ad",
|
||||
},
|
||||
{
|
||||
entropy: "77c2b00716cec7213839159e404db50d",
|
||||
mnemonic: "jelly better achieve collect unaware mountain thought cargo oxygen act hood bridge",
|
||||
seed: "b5b6d0127db1a9d2226af0c3346031d77af31e918dba64287a1b44b8ebf63cdd52676f672a290aae502472cf2d602c051f3e6f18055e84e4c43897fc4e51a6ff",
|
||||
},
|
||||
{
|
||||
entropy: "b63a9c59a6e641f288ebc103017f1da9f8290b3da6bdef7b",
|
||||
mnemonic: "renew stay biology evidence goat welcome casual join adapt armor shuffle fault little machine walk stumble urge swap",
|
||||
seed: "9248d83e06f4cd98debf5b6f010542760df925ce46cf38a1bdb4e4de7d21f5c39366941c69e1bdbf2966e0f6e6dbece898a0e2f0a4c2b3e640953dfe8b7bbdc5",
|
||||
},
|
||||
{
|
||||
entropy: "3e141609b97933b66a060dcddc71fad1d91677db872031e85f4c015c5e7e8982",
|
||||
mnemonic: "dignity pass list indicate nasty swamp pool script soccer toe leaf photo multiply desk host tomato cradle drill spread actor shine dismiss champion exotic",
|
||||
seed: "ff7f3184df8696d8bef94b6c03114dbee0ef89ff938712301d27ed8336ca89ef9635da20af07d4175f2bf5f3de130f39c9d9e8dd0472489c19b1a020a940da67",
|
||||
},
|
||||
{
|
||||
entropy: "0460ef47585604c5660618db2e6a7e7f",
|
||||
mnemonic: "afford alter spike radar gate glance object seek swamp infant panel yellow",
|
||||
seed: "65f93a9f36b6c85cbe634ffc1f99f2b82cbb10b31edc7f087b4f6cb9e976e9faf76ff41f8f27c99afdf38f7a303ba1136ee48a4c1e7fcd3dba7aa876113a36e4",
|
||||
},
|
||||
{
|
||||
entropy: "72f60ebac5dd8add8d2a25a797102c3ce21bc029c200076f",
|
||||
mnemonic: "indicate race push merry suffer human cruise dwarf pole review arch keep canvas theme poem divorce alter left",
|
||||
seed: "3bbf9daa0dfad8229786ace5ddb4e00fa98a044ae4c4975ffd5e094dba9e0bb289349dbe2091761f30f382d4e35c4a670ee8ab50758d2c55881be69e327117ba",
|
||||
},
|
||||
{
|
||||
entropy: "2c85efc7f24ee4573d2b81a6ec66cee209b2dcbd09d8eddc51e0215b0b68e416",
|
||||
mnemonic: "clutch control vehicle tonight unusual clog visa ice plunge glimpse recipe series open hour vintage deposit universe tip job dress radar refuse motion taste",
|
||||
seed: "fe908f96f46668b2d5b37d82f558c77ed0d69dd0e7e043a5b0511c48c2f1064694a956f86360c93dd04052a8899497ce9e985ebe0c8c52b955e6ae86d4ff4449",
|
||||
},
|
||||
{
|
||||
entropy: "eaebabb2383351fd31d703840b32e9e2",
|
||||
mnemonic: "turtle front uncle idea crush write shrug there lottery flower risk shell",
|
||||
seed: "bdfb76a0759f301b0b899a1e3985227e53b3f51e67e3f2a65363caedf3e32fde42a66c404f18d7b05818c95ef3ca1e5146646856c461c073169467511680876c",
|
||||
},
|
||||
{
|
||||
entropy: "7ac45cfe7722ee6c7ba84fbc2d5bd61b45cb2fe5eb65aa78",
|
||||
mnemonic: "kiss carry display unusual confirm curtain upgrade antique rotate hello void custom frequent obey nut hole price segment",
|
||||
seed: "ed56ff6c833c07982eb7119a8f48fd363c4a9b1601cd2de736b01045c5eb8ab4f57b079403485d1c4924f0790dc10a971763337cb9f9c62226f64fff26397c79",
|
||||
},
|
||||
{
|
||||
entropy: "4fa1a8bc3e6d80ee1316050e862c1812031493212b7ec3f3bb1b08f168cabeef",
|
||||
mnemonic: "exile ask congress lamp submit jacket era scheme attend cousin alcohol catch course end lucky hurt sentence oven short ball bird grab wing top",
|
||||
seed: "095ee6f817b4c2cb30a5a797360a81a40ab0f9a4e25ecd672a3f58a0b5ba0687c096a6b14d2c0deb3bdefce4f61d01ae07417d502429352e27695163f7447a8c",
|
||||
},
|
||||
{
|
||||
entropy: "18ab19a9f54a9274f03e5209a2ac8a91",
|
||||
mnemonic: "board flee heavy tunnel powder denial science ski answer betray cargo cat",
|
||||
seed: "6eff1bb21562918509c73cb990260db07c0ce34ff0e3cc4a8cb3276129fbcb300bddfe005831350efd633909f476c45c88253276d9fd0df6ef48609e8bb7dca8",
|
||||
},
|
||||
{
|
||||
entropy: "18a2e1d81b8ecfb2a333adcb0c17a5b9eb76cc5d05db91a4",
|
||||
mnemonic: "board blade invite damage undo sun mimic interest slam gaze truly inherit resist great inject rocket museum chief",
|
||||
seed: "f84521c777a13b61564234bf8f8b62b3afce27fc4062b51bb5e62bdfecb23864ee6ecf07c1d5a97c0834307c5c852d8ceb88e7c97923c0a3b496bedd4e5f88a9",
|
||||
},
|
||||
{
|
||||
entropy: "15da872c95a13dd738fbf50e427583ad61f18fd99f628c417a61cf8343c90419",
|
||||
mnemonic: "beyond stage sleep clip because twist token leaf atom beauty genius food business side grid unable middle armed observe pair crouch tonight away coconut",
|
||||
seed: "b15509eaa2d09d3efd3e006ef42151b30367dc6e3aa5e44caba3fe4d3e352e65101fbdb86a96776b91946ff06f8eac594dc6ee1d3e82a42dfe1b40fef6bcc3fd",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:dupword,lll // the test vectors, kept as upstream wrote them
|
||||
func badMnemonicSentences() []vector {
|
||||
return []vector{
|
||||
{mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon"},
|
||||
{mnemonic: "legal winner thank year wave sausage worth useful legal winner thank yellow yellow"},
|
||||
{mnemonic: "letter advice cage absurd amount doctor acoustic avoid letter advice caged above"},
|
||||
{mnemonic: "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo, wrong"},
|
||||
{mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon"},
|
||||
{mnemonic: "legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal will will will"},
|
||||
{mnemonic: "letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic avoid letter always."},
|
||||
{mnemonic: "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo why"},
|
||||
{mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art art"},
|
||||
{mnemonic: "legal winner thank year wave sausage worth useful legal winner thanks year wave worth useful legal winner thank year wave sausage worth title"},
|
||||
{mnemonic: "letter advice cage absurd amount doctor acoustic avoid letters advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic bless"},
|
||||
{mnemonic: "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo voted"},
|
||||
{mnemonic: "jello better achieve collect unaware mountain thought cargo oxygen act hood bridge"},
|
||||
{mnemonic: "renew, stay, biology, evidence, goat, welcome, casual, join, adapt, armor, shuffle, fault, little, machine, walk, stumble, urge, swap"},
|
||||
{mnemonic: "dignity pass list indicate nasty"},
|
||||
|
||||
// From issue 32
|
||||
{mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon letter"},
|
||||
}
|
||||
}
|
||||
|
||||
func assertNil(t *testing.T, object any) {
|
||||
t.Helper()
|
||||
|
||||
if object != nil {
|
||||
t.Errorf("Expected nil, got %v", object)
|
||||
}
|
||||
}
|
||||
|
||||
func assertNotNil(t *testing.T, object any) {
|
||||
t.Helper()
|
||||
|
||||
if object == nil {
|
||||
t.Error("Expected not nil")
|
||||
}
|
||||
}
|
||||
|
||||
func assertTrue(t *testing.T, a bool) {
|
||||
t.Helper()
|
||||
|
||||
if !a {
|
||||
t.Error("Expected true, got false")
|
||||
}
|
||||
}
|
||||
|
||||
func assertFalse(t *testing.T, a bool) {
|
||||
t.Helper()
|
||||
|
||||
if a {
|
||||
t.Error("Expected false, got true")
|
||||
}
|
||||
}
|
||||
|
||||
func assertEqual(t *testing.T, a, b any) {
|
||||
t.Helper()
|
||||
|
||||
if a != b {
|
||||
t.Errorf("Objects not equal, expected `%s` and got `%s`", a, b)
|
||||
}
|
||||
}
|
||||
|
||||
func assertEqualString(t *testing.T, a, b string) {
|
||||
t.Helper()
|
||||
|
||||
if a != b {
|
||||
t.Errorf("Strings not equal, expected `%s` and got `%s`", a, b)
|
||||
}
|
||||
}
|
||||
|
||||
func assertEqualByteSlices(t *testing.T, a, b []byte) {
|
||||
t.Helper()
|
||||
|
||||
if len(a) != len(b) {
|
||||
t.Errorf("Byte slices not equal, expected %v and got %v", a, b)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
for i := range a {
|
||||
if a[i] != b[i] {
|
||||
t.Errorf("Byte slices not equal, expected %v and got %v", a, b)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,19 +0,0 @@
|
||||
package bip39
|
||||
|
||||
import (
|
||||
"hash/crc32"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEnglishChecksum(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Ensure word list is correct
|
||||
// $ wget https://raw.githubusercontent.com/bitcoin/bips/master/bip-0039/english.txt
|
||||
// $ crc32 english.txt
|
||||
// c1dbd296
|
||||
checksum := crc32.ChecksumIEEE([]byte(english))
|
||||
if checksum != 0xc1dbd296 {
|
||||
t.Error("english checksum invalid")
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package bip39_test
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
|
||||
"sneak.berlin/go/keyfunc/internal/bip39"
|
||||
)
|
||||
|
||||
//nolint:lll // the test vector and its output, kept as upstream wrote them
|
||||
func ExampleNewMnemonic() {
|
||||
// the entropy can be any byte slice, generated how pleased,
|
||||
// as long its bit size is a multiple of 32 and is within
|
||||
// the inclusive range of {128,256}
|
||||
entropy, _ := hex.DecodeString("066dca1a2bb7e8a1db2832148ce9933eea0f3ac9548d793112d9a95c9407efad")
|
||||
|
||||
// generate a mnemomic
|
||||
mnemomic, _ := bip39.NewMnemonic(entropy)
|
||||
fmt.Println(mnemomic)
|
||||
// output:
|
||||
// all hour make first leader extend hole alien behind guard gospel lava path output census museum junior mass reopen famous sing advance salt reform
|
||||
}
|
||||
|
||||
//nolint:lll // the test vector and its output, kept as upstream wrote them
|
||||
func ExampleNewSeed() {
|
||||
seed := bip39.NewSeed("all hour make first leader extend hole alien behind guard gospel lava path output census museum junior mass reopen famous sing advance salt reform", "TREZOR")
|
||||
fmt.Println(hex.EncodeToString(seed))
|
||||
// output:
|
||||
// 26e975ec644423f4a4c4f4215ef09b4bd7ef924e85d1d17c4cf3f136c2863cf6df0a475045652c57eb5fb41513ca2a2d67722b77e954b4b3fc11f7590449191d
|
||||
}
|
||||
@@ -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"
|
||||
bip39 "github.com/tyler-smith/go-bip39"
|
||||
"sneak.berlin/go/keyfunc/internal/derive"
|
||||
"sneak.berlin/go/secret/pkg/bip85"
|
||||
)
|
||||
|
||||
// english is the number BIP-85 gives the English word list.
|
||||
@@ -29,17 +29,6 @@ const DefaultWords = twelve
|
||||
// ErrWordCount is returned for a length this tool does not offer.
|
||||
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
|
||||
// key index, taken from the master key with BIP-85's own mnemonic
|
||||
// application, number 39. The words come straight from the BIP-85
|
||||
@@ -49,12 +38,13 @@ func Derive(
|
||||
master *hdkeychain.ExtendedKey,
|
||||
words, index uint32,
|
||||
) (string, error) {
|
||||
err := CheckWords(words)
|
||||
if err != nil {
|
||||
return "", err
|
||||
switch words {
|
||||
case twelve, eighteen, twentyFour:
|
||||
default:
|
||||
return "", fmt.Errorf("%w, not %d", ErrWordCount, words)
|
||||
}
|
||||
|
||||
err = derive.CheckIndex(index)
|
||||
err := derive.CheckIndex(index)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil/hdkeychain"
|
||||
"github.com/stretchr/testify/require"
|
||||
"sneak.berlin/go/keyfunc/internal/bip39"
|
||||
bip39 "github.com/tyler-smith/go-bip39"
|
||||
"sneak.berlin/go/keyfunc/internal/childmnemonic"
|
||||
"sneak.berlin/go/keyfunc/internal/derive"
|
||||
)
|
||||
|
||||
+27
-43
@@ -108,45 +108,47 @@ func decrypt() *cobra.Command {
|
||||
}
|
||||
|
||||
// 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 {
|
||||
names, err := cmd.Flags().GetStringArray("to")
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading the recipients: %w", err)
|
||||
}
|
||||
|
||||
to, err := agekey.Recipients(names)
|
||||
key, err := derived(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
to, err := cmd.Flags().GetStringArray("to")
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading the recipients: %w", err)
|
||||
}
|
||||
|
||||
armored, err := cmd.Flags().GetBool("armor")
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading the armor flag: %w", err)
|
||||
}
|
||||
|
||||
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, func(dst io.Writer, plain io.Reader) error {
|
||||
return key.Encrypt(dst, plain, to, armored)
|
||||
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. The input is opened
|
||||
// before the key is derived, as for runEncrypt.
|
||||
// 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)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -154,24 +156,6 @@ func runDecrypt(cmd *cobra.Command, args []string) error {
|
||||
|
||||
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")
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading the output file: %w", err)
|
||||
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
"sneak.berlin/go/keyfunc/internal/agekey"
|
||||
"sneak.berlin/go/keyfunc/internal/cli"
|
||||
"sneak.berlin/go/keyfunc/internal/cli/age"
|
||||
"sneak.berlin/go/keyfunc/internal/derive"
|
||||
"sneak.berlin/go/keyfunc/internal/mnemonic"
|
||||
)
|
||||
|
||||
@@ -70,44 +69,6 @@ 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) {
|
||||
t.Setenv(mnemonic.Variable, example())
|
||||
|
||||
|
||||
@@ -2,13 +2,12 @@ package cli_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
bip39 "github.com/tyler-smith/go-bip39"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"sneak.berlin/go/keyfunc/internal/bip39"
|
||||
"sneak.berlin/go/keyfunc/internal/childmnemonic"
|
||||
"sneak.berlin/go/keyfunc/internal/cli"
|
||||
"sneak.berlin/go/keyfunc/internal/derive"
|
||||
@@ -34,14 +33,6 @@ const (
|
||||
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:
|
||||
// eleven abandons and about.
|
||||
func example() string {
|
||||
@@ -107,75 +98,12 @@ func TestThePrivateKeyMatchesThePublicOne(t *testing.T) {
|
||||
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) {
|
||||
t.Setenv(mnemonic.CommandVariable, failingMnemonicCommand)
|
||||
|
||||
for _, args := range [][]string{
|
||||
{subcommand, "pub", "-n", tooLarge},
|
||||
{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) {
|
||||
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)
|
||||
})
|
||||
}
|
||||
out, err := execute(t, "ssh", "pub", "-n", "2147483648")
|
||||
require.ErrorIs(t, err, derive.ErrIndexTooLarge)
|
||||
require.Empty(t, out)
|
||||
}
|
||||
|
||||
func TestTheMnemonicCommandIsUsed(t *testing.T) {
|
||||
@@ -202,32 +130,14 @@ func TestAChildMnemonicIsPrinted(t *testing.T) {
|
||||
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) {
|
||||
t.Setenv(mnemonic.CommandVariable, failingMnemonicCommand)
|
||||
t.Setenv(mnemonic.Variable, example())
|
||||
|
||||
out, err := execute(t, "mnemonic", "--words", "15")
|
||||
require.ErrorIs(t, err, childmnemonic.ErrWordCount)
|
||||
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 {
|
||||
|
||||
@@ -31,9 +31,8 @@ func Command() *cobra.Command {
|
||||
},
|
||||
}
|
||||
|
||||
words := options.Decimal(childmnemonic.DefaultWords)
|
||||
cmd.Flags().Var(
|
||||
&words, "words",
|
||||
cmd.Flags().Uint32(
|
||||
"words", childmnemonic.DefaultWords,
|
||||
"how long the child mnemonic is: 12, 18 or 24 words",
|
||||
)
|
||||
|
||||
@@ -52,11 +51,6 @@ func derived(cmd *cobra.Command) (string, error) {
|
||||
return "", fmt.Errorf("reading the word count: %w", err)
|
||||
}
|
||||
|
||||
err = childmnemonic.CheckWords(words)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
parent, err := options.Mnemonic(cmd)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
||||
@@ -4,10 +4,8 @@ package options
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"sneak.berlin/go/keyfunc/internal/derive"
|
||||
"sneak.berlin/go/keyfunc/internal/mnemonic"
|
||||
)
|
||||
|
||||
@@ -18,26 +16,19 @@ func Add(cmd *cobra.Command) {
|
||||
"mnemonic-command", "",
|
||||
"shell command whose output is the mnemonic",
|
||||
)
|
||||
cmd.PersistentFlags().VarP(
|
||||
new(Decimal), "index", "n",
|
||||
cmd.PersistentFlags().Uint32P(
|
||||
"index", "n", 0,
|
||||
"which key to derive",
|
||||
)
|
||||
}
|
||||
|
||||
// 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.
|
||||
// Index returns the key index the user asked for.
|
||||
func Index(cmd *cobra.Command) (uint32, error) {
|
||||
index, err := cmd.Flags().GetUint32("index")
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("reading the index: %w", err)
|
||||
}
|
||||
|
||||
err = derive.CheckIndex(index)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return index, nil
|
||||
}
|
||||
|
||||
@@ -50,33 +41,3 @@ 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"
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"sneak.berlin/go/keyfunc/internal/cli/signals"
|
||||
"sneak.berlin/go/keyfunc/internal/mnemonic"
|
||||
)
|
||||
|
||||
// Where the key goes on the host and what the file it arrives in is
|
||||
@@ -221,7 +220,7 @@ func session(
|
||||
|
||||
//nolint:gosec // the options are the user's own, meant for sftp
|
||||
command := exec.CommandContext(cmd.Context(), "sftp", argv...)
|
||||
command.Env = mnemonic.ChildEnv()
|
||||
command.Env = childEnv()
|
||||
command.Stdin = strings.NewReader(strings.Join(batch, "\n") + "\n")
|
||||
command.Stdout = &said
|
||||
command.Stderr = &said
|
||||
|
||||
@@ -3,10 +3,13 @@ package ssh
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"sneak.berlin/go/keyfunc/internal/cli/options"
|
||||
"sneak.berlin/go/keyfunc/internal/derive"
|
||||
"sneak.berlin/go/keyfunc/internal/mnemonic"
|
||||
"sneak.berlin/go/keyfunc/internal/sshkey"
|
||||
)
|
||||
|
||||
@@ -84,6 +87,26 @@ func write(cmd *cobra.Command, text string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// childEnv is the tool's environment with the mnemonic variables taken
|
||||
// out, for the ssh and sftp children it starts. "ssh to" exists so the
|
||||
// private key never leaves the tool; the mnemonic, from either variable,
|
||||
// must not leave it either.
|
||||
func childEnv() []string {
|
||||
environ := os.Environ()
|
||||
kept := make([]string, 0, len(environ))
|
||||
|
||||
for _, entry := range environ {
|
||||
name, _, _ := strings.Cut(entry, "=")
|
||||
if name == mnemonic.Variable || name == mnemonic.CommandVariable {
|
||||
continue
|
||||
}
|
||||
|
||||
kept = append(kept, entry)
|
||||
}
|
||||
|
||||
return kept
|
||||
}
|
||||
|
||||
// addComment gives a command its comment flag.
|
||||
func addComment(cmd *cobra.Command) {
|
||||
cmd.Flags().String(
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"sneak.berlin/go/keyfunc/internal/cli/signals"
|
||||
"sneak.berlin/go/keyfunc/internal/mnemonic"
|
||||
)
|
||||
|
||||
// StatusError says the tool should end with the status ssh ended with.
|
||||
@@ -58,13 +57,8 @@ func to() *cobra.Command {
|
||||
|
||||
defer served.Stop()
|
||||
|
||||
// IdentitiesOnly yes in the user's ssh configuration
|
||||
// would keep the agent's key from being offered. The
|
||||
// agent holds only the derived key, so turning it off
|
||||
// adds no other key.
|
||||
argv := slices.Concat([]string{
|
||||
"-o", "IdentityAgent=" + served.Socket(),
|
||||
"-o", "IdentitiesOnly=no",
|
||||
}, args)
|
||||
|
||||
return connect(ctx, argv)
|
||||
@@ -85,7 +79,7 @@ func to() *cobra.Command {
|
||||
func connect(ctx context.Context, argv []string) error {
|
||||
//nolint:gosec // the arguments are the user's own, meant for ssh
|
||||
command := exec.CommandContext(ctx, "ssh", argv...)
|
||||
command.Env = mnemonic.ChildEnv()
|
||||
command.Env = childEnv()
|
||||
command.Stdin = os.Stdin
|
||||
command.Stdout = os.Stdout
|
||||
command.Stderr = os.Stderr
|
||||
|
||||
@@ -587,7 +587,7 @@ func TestSSHIsPointedAtTheAgentAndItsStatusIsHandedOn(t *testing.T) {
|
||||
|
||||
given := recorded(t, arguments)
|
||||
require.Equal(t, "-o", given[0])
|
||||
require.Equal(t, []string{host, remoteCommand}, given[4:])
|
||||
require.Equal(t, []string{host, remoteCommand}, given[2:])
|
||||
|
||||
// The stand-in wrote the path down only because there really was
|
||||
// a socket there while it ran.
|
||||
@@ -596,26 +596,6 @@ func TestSSHIsPointedAtTheAgentAndItsStatusIsHandedOn(t *testing.T) {
|
||||
require.NoDirExists(t, filepath.Dir(socket))
|
||||
}
|
||||
|
||||
func TestIdentitiesOnlyIsTurnedOffBeforeTheHost(t *testing.T) {
|
||||
t.Setenv(mnemonic.Variable, example())
|
||||
|
||||
arguments, _ := pretendCall(t)
|
||||
|
||||
_, err := execute(t, subcommand, "to", host, remoteCommand)
|
||||
|
||||
var passed ssh.StatusError
|
||||
|
||||
require.ErrorAs(t, err, &passed)
|
||||
|
||||
// IdentitiesOnly yes in the user's ssh configuration keeps ssh
|
||||
// from offering the agent's key. An option given before the host
|
||||
// wins over the configuration.
|
||||
given := recorded(t, arguments)
|
||||
require.Equal(t,
|
||||
[]string{"-o", "IdentitiesOnly=no", host, remoteCommand}, given[2:],
|
||||
)
|
||||
}
|
||||
|
||||
func TestTheToolEndsWithTheStatusSSHEndedWith(t *testing.T) {
|
||||
t.Setenv(mnemonic.Variable, example())
|
||||
|
||||
|
||||
@@ -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"
|
||||
bip39 "github.com/tyler-smith/go-bip39"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -8,11 +8,10 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
bip39 "github.com/tyler-smith/go-bip39"
|
||||
"golang.org/x/term"
|
||||
"sneak.berlin/go/keyfunc/internal/bip39"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -30,28 +29,6 @@ var ErrMissing = errors.New(
|
||||
"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
|
||||
// checksum.
|
||||
var ErrChecksum = errors.New("the mnemonic fails its BIP-39 checksum")
|
||||
@@ -86,7 +63,6 @@ func Read(ctx context.Context, command string) (string, error) {
|
||||
func run(ctx context.Context, command string) (string, error) {
|
||||
//nolint:gosec // running the user's own command is the point
|
||||
shell := exec.CommandContext(ctx, "sh", "-c", command)
|
||||
shell.Env = ChildEnv()
|
||||
|
||||
var complaint bytes.Buffer
|
||||
|
||||
@@ -107,25 +83,6 @@ func run(ctx context.Context, command string) (string, error) {
|
||||
return string(out), nil
|
||||
}
|
||||
|
||||
// ChildEnv is the tool's environment with the mnemonic variables taken
|
||||
// out, for the programs the tool starts: the mnemonic command, ssh and
|
||||
// sftp. The mnemonic, from either variable, never leaves the tool.
|
||||
func ChildEnv() []string {
|
||||
environ := os.Environ()
|
||||
kept := make([]string, 0, len(environ))
|
||||
|
||||
for _, entry := range environ {
|
||||
name, _, _ := strings.Cut(entry, "=")
|
||||
if name == Variable || name == CommandVariable {
|
||||
continue
|
||||
}
|
||||
|
||||
kept = append(kept, entry)
|
||||
}
|
||||
|
||||
return kept
|
||||
}
|
||||
|
||||
// ask prompts on the terminal with echo turned off.
|
||||
func ask() (string, error) {
|
||||
fd := int(os.Stdin.Fd())
|
||||
@@ -149,34 +106,13 @@ func ask() (string, error) {
|
||||
|
||||
// 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 is not a valid BIP-39 mnemonic, with an
|
||||
// error that says what is wrong with it.
|
||||
func checked(text string) (string, error) {
|
||||
words := strings.Fields(text)
|
||||
// and refuses a mnemonic that does not pass the BIP-39 checksum.
|
||||
func checked(words string) (string, error) {
|
||||
words = strings.Join(strings.Fields(words), " ")
|
||||
|
||||
switch len(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) {
|
||||
if !bip39.IsMnemonicValid(words) {
|
||||
return "", ErrChecksum
|
||||
}
|
||||
|
||||
return joined, nil
|
||||
return words, nil
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package mnemonic_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -63,31 +61,6 @@ func TestTheMnemonicInTheEnvironmentComesLast(t *testing.T) {
|
||||
require.Equal(t, fromVariable, words)
|
||||
}
|
||||
|
||||
func TestTheMnemonicIsNotHandedToTheCommand(t *testing.T) {
|
||||
const marker = "KEYFUNC_TEST_MARKER"
|
||||
|
||||
t.Setenv(mnemonic.CommandVariable, prints(fromCommandVariable))
|
||||
t.Setenv(mnemonic.Variable, fromVariable)
|
||||
t.Setenv(marker, "reaches the command")
|
||||
|
||||
path := filepath.Join(t.TempDir(), "environment")
|
||||
|
||||
words, err := mnemonic.Read(
|
||||
t.Context(), "env > '"+path+"'; "+prints(example()),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, example(), words)
|
||||
|
||||
//nolint:gosec // the path is a temporary file of the test's own
|
||||
content, err := os.ReadFile(path)
|
||||
require.NoError(t, err)
|
||||
|
||||
environment := string(content)
|
||||
require.NotContains(t, environment, mnemonic.Variable+"=")
|
||||
require.NotContains(t, environment, mnemonic.CommandVariable+"=")
|
||||
require.Contains(t, environment, marker+"=")
|
||||
}
|
||||
|
||||
func TestAFailingCommandSaysWhatWentWrong(t *testing.T) {
|
||||
t.Setenv(mnemonic.CommandVariable, "")
|
||||
t.Setenv(mnemonic.Variable, "")
|
||||
@@ -97,27 +70,6 @@ func TestAFailingCommandSaysWhatWentWrong(t *testing.T) {
|
||||
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) {
|
||||
t.Setenv(mnemonic.CommandVariable, "")
|
||||
t.Setenv(mnemonic.Variable, broken())
|
||||
@@ -126,38 +78,6 @@ func TestABadChecksumIsRefused(t *testing.T) {
|
||||
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) {
|
||||
t.Setenv(mnemonic.CommandVariable, "")
|
||||
t.Setenv(mnemonic.Variable, "")
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"prettier": "3.8.1"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user