Compare commits
1
Commits
next
..
72cb05b5a8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72cb05b5a8 |
+6
-13
@@ -18,16 +18,9 @@
|
|||||||
# does not need .git/config; that file can hold a credential, such as a
|
# 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.
|
# 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
|
# 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
|
# under .git/modules/, nested again for a submodule's own submodules.
|
||||||
# its own .git directory when it keeps one.
|
.git/config
|
||||||
# KNOWN GAP: a submodule whose name has a `config` segment (`config`,
|
.git/modules/**/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
|
|
||||||
|
|
||||||
# Agent scratch: one full checkout of the repo per in-flight agent.
|
# Agent scratch: one full checkout of the repo per in-flight agent.
|
||||||
# Anchored because it occurs once where agents run at the repo root.
|
# Anchored because it occurs once where agents run at the repo root.
|
||||||
@@ -51,9 +44,7 @@
|
|||||||
**/[iI][dD]_[rR][sS][aA]
|
**/[iI][dD]_[rR][sS][aA]
|
||||||
**/[iI][dD]_[dD][sS][aA]
|
**/[iI][dD]_[dD][sS][aA]
|
||||||
**/[iI][dD]_[eE][cC][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
|
||||||
**/[iI][dD]_[eE][dD]25519_[sS][kK]
|
|
||||||
|
|
||||||
# Dependencies: restored inside the image, never copied in.
|
# Dependencies: restored inside the image, never copied in.
|
||||||
**/node_modules
|
**/node_modules
|
||||||
@@ -71,5 +62,7 @@
|
|||||||
**/.vscode
|
**/.vscode
|
||||||
**/*.sublime-*
|
**/*.sublime-*
|
||||||
|
|
||||||
# The binary `make build` writes.
|
# The binary `make build` writes, and the CI workflow, which is not a
|
||||||
|
# build input.
|
||||||
/keyfunc
|
/keyfunc
|
||||||
|
.gitea
|
||||||
|
|||||||
@@ -42,9 +42,7 @@ node_modules/
|
|||||||
[iI][dD]_[rR][sS][aA]
|
[iI][dD]_[rR][sS][aA]
|
||||||
[iI][dD]_[dD][sS][aA]
|
[iI][dD]_[dD][sS][aA]
|
||||||
[iI][dD]_[eE][cC][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
|
||||||
[iI][dD]_[eE][dD]25519_[sS][kK]
|
|
||||||
|
|
||||||
# The binary `make build` writes.
|
# The binary `make build` writes.
|
||||||
/keyfunc
|
/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
|
mnemonic at any time. The same mnemonic, key type and index always give the same
|
||||||
key.
|
key.
|
||||||
|
|
||||||
It uses the BIP-85 entropy deriver from `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.
|
takes the same steps as that repository's `agehd` package.
|
||||||
|
|
||||||
Commands are grouped by what is derived: `keyfunc ssh ...` for ed25519 SSH keys,
|
Commands are grouped by what is derived: `keyfunc ssh ...` for ed25519 SSH keys,
|
||||||
@@ -66,11 +66,8 @@ calls into `internal/`. The packages there are:
|
|||||||
- `internal/childmnemonic` derives a child mnemonic from the main one using
|
- `internal/childmnemonic` derives a child mnemonic from the main one using
|
||||||
BIP-85's own mnemonic application.
|
BIP-85's own mnemonic application.
|
||||||
- `internal/cli` builds the cobra command tree and runs it. Under it,
|
- `internal/cli` builds the cobra command tree and runs it. Under it,
|
||||||
`cli/options` holds the flags every command shares, `cli/signals` catches
|
`cli/options` holds the flags every command shares, and `cli/ssh`, `cli/age`
|
||||||
SIGINT, SIGTERM and SIGHUP for the commands that clean up before they end, and
|
and `cli/mnemonic` are the command groups.
|
||||||
`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
|
### Adding a key type
|
||||||
|
|
||||||
@@ -179,13 +176,10 @@ same way: one that cannot be read fails the first listing, and one that can be
|
|||||||
read but not entered fails the second, after which the tool says that `~/.ssh`
|
read but not entered fails the second, after which the tool says that `~/.ssh`
|
||||||
cannot be entered. The wording of a missing file elsewhere does not count
|
cannot be entered. The wording of a missing file elsewhere does not count
|
||||||
either, since `ssh` writes `No such file or directory` about an `-i` it cannot
|
either, since `ssh` writes `No such file or directory` about an `-i` it cannot
|
||||||
find on a session that then authenticates through the agent. An
|
find on a session that then authenticates through the agent. If an identical
|
||||||
`authorized_keys` that the first listing shows to be a symlink is refused and
|
line is already in the file, the tool prints `already present` and connects no
|
||||||
left as it is, since the rename below would replace the link itself and the file
|
further. Otherwise the line is added (after a newline, if the file did not end
|
||||||
it points at would never get the key. If an identical line is already in the
|
with one) and a second connection:
|
||||||
file, the tool prints `already present` and connects no further. Otherwise the
|
|
||||||
line is added (after a newline, if the file did not end with one) and a second
|
|
||||||
connection:
|
|
||||||
|
|
||||||
- makes `~/.ssh` and sets it to mode `0700`, but only when the first connection
|
- makes `~/.ssh` and sets it to mode `0700`, but only when the first connection
|
||||||
found none; a `~/.ssh` that was already there keeps the mode it had;
|
found none; a `~/.ssh` that was already there keeps the mode it had;
|
||||||
@@ -255,21 +249,11 @@ identity's own recipient, plus any given with `--to`, so the same mnemonic can
|
|||||||
always decrypt what it encrypted. Output goes to `-o` or standard output;
|
always decrypt what it encrypted. Output goes to `-o` or standard output;
|
||||||
`--armor` writes the text form. Nothing is written except the output.
|
`--armor` writes the text form. Nothing is written except the output.
|
||||||
|
|
||||||
A `-o` path that is the same file as the tool's own standard output or standard
|
|
||||||
error, under any name such as `/dev/stdout` or `/dev/fd/2`, is written to that
|
|
||||||
stream, as leaving out `-o` writes to standard output; the file the stream is
|
|
||||||
redirected to is written as the redirect says and never replaced, so with `>>`
|
|
||||||
the output follows what the file already held. Otherwise, a regular file already
|
|
||||||
at the `-o` path is replaced, and the new file has mode `0600`. A symlink there
|
|
||||||
is followed, and what it points at is treated the same way, so the link keeps
|
|
||||||
pointing where it did; a symlink that points at nothing is refused. A named pipe
|
|
||||||
or a device, such as `/dev/null`, is written to directly.
|
|
||||||
|
|
||||||
### `keyfunc age decrypt [-n N] [-o <file>] [<file>]`
|
### `keyfunc age decrypt [-n N] [-o <file>] [<file>]`
|
||||||
|
|
||||||
Decrypts the file (or standard input) with the derived identity. Output goes to
|
Decrypts the file (or standard input) with the derived identity. Output goes to
|
||||||
`-o`, which is treated as for `encrypt`, or standard output. If the identity is
|
`-o` or standard output. If the identity is not one of the recipients, the tool
|
||||||
not one of the recipients, the tool says so and exits with status 1.
|
says so and exits with status 1.
|
||||||
|
|
||||||
## Derived mnemonics: `keyfunc mnemonic`
|
## Derived mnemonics: `keyfunc mnemonic`
|
||||||
|
|
||||||
@@ -304,21 +288,6 @@ girl mad pet galaxy egg matter matrix prison refuse sense ordinary nose
|
|||||||
Errors go to standard error and the exit status is 1, except for `ssh to`, which
|
Errors go to standard error and the exit status is 1, except for `ssh to`, which
|
||||||
passes through `ssh`'s own exit status.
|
passes through `ssh`'s own exit status.
|
||||||
|
|
||||||
SIGINT, SIGTERM and SIGHUP end any command at once, at the mnemonic prompt too,
|
|
||||||
with the status a shell gives a program killed by that signal (130 for SIGINT).
|
|
||||||
While `age encrypt -o` or `age decrypt -o` is writing a new file or replacing a
|
|
||||||
regular one, the signal makes it remove the unfinished file, leave a file
|
|
||||||
already at the named path as it was, and exit with status 1. That holds for a
|
|
||||||
signal that has reached `keyfunc` when its input ends; a later one leaves the
|
|
||||||
whole file in place. Ctrl-C on a pipeline ends the input at the same moment, and
|
|
||||||
on Linux `keyfunc` sees the signal first, though no system promises that. A
|
|
||||||
named pipe or a device at the `-o` path, or a path that is the same file as
|
|
||||||
standard output or standard error, is written to directly, and the signal ends
|
|
||||||
the tool there as it ends any other command. While `ssh to` or `ssh install` has
|
|
||||||
`ssh` or `sftp` running, the signal ends that program instead, the tool removes
|
|
||||||
its agent socket or working files, and it exits with status 1, or for `ssh to`
|
|
||||||
with `ssh`'s own status if `ssh` reported one.
|
|
||||||
|
|
||||||
## Entrypoints
|
## Entrypoints
|
||||||
|
|
||||||
The repo adheres to the
|
The repo adheres to the
|
||||||
@@ -375,7 +344,9 @@ standard: most Makefile targets are thin shims over an executable in `script/`
|
|||||||
|
|
||||||
## TODO
|
## 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
|
## 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
|
`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
|
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.
|
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
|
Dockerfiles install development prerequisites by running `script/bootstrap`
|
||||||
install what those images lack either inline, as the canonical Go `Dockerfile`
|
rather than duplicating installs inline; COPY `script/` and the dependency
|
||||||
below does for `git`, or by running `script/bootstrap`, as the `prompts`
|
manifests (`package.json` + `yarn.lock`, `go.mod` + `go.sum`, etc.) before
|
||||||
repo's own `Dockerfile` does for its yarn packages. The development
|
running it.
|
||||||
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.
|
|
||||||
|
|
||||||
- **Linting and testing run in Docker, as phases of the `Dockerfile`.** There is
|
- **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
|
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
|
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`
|
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.
|
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 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
|
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
|
(e.g. a web frontend compiled in a separate stage), the lint phase must
|
||||||
create placeholder files so the embed directives resolve. Example:
|
create placeholder files so the embed directives resolve. Example:
|
||||||
`RUN mkdir -p web/dist && touch web/dist/index.html web/dist/style.css`.
|
`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
|
- If the project requires CGO or system libraries for linting (e.g.
|
||||||
in the lint phase. The `golangci/golangci-lint` image is Debian-based and
|
`vips-dev`), install them in the lint phase with `apk add`.
|
||||||
has no `apk`, so install with `apt-get` under the Debian package name
|
- `.dockerignore` lets `.git` into the build context. It keeps out
|
||||||
(`libvips-dev`, where alpine says `vips-dev`), and delete the package
|
`.git/config` and each submodule's `config` under `.git/modules/` at any
|
||||||
lists in the same `RUN`, so the layer does not keep them:
|
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
|
||||||
```dockerfile
|
CI checkout step stores there. The stage that compiles has `git` (the
|
||||||
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
|
|
||||||
Debian Go image has it; an alpine one needs `apk add --no-cache git`) and
|
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,
|
takes the version from the `VERSION` build argument when one is given,
|
||||||
otherwise from `git describe --tags --always`. That gives the tag on a
|
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
|
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
|
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
|
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
|
from a run of its own gates rather than from a cache entry.
|
||||||
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.
|
|
||||||
|
|
||||||
- Use platform-standard formatters: `black` for Python, `prettier` for
|
- Use platform-standard formatters: `black` for Python, `prettier` for
|
||||||
JS/CSS/Markdown/HTML, `go fmt` for Go. Always use default configuration with
|
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`.
|
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
|
- When pinning images or packages by hash, add a comment above the reference
|
||||||
with the version and date (YYYY-MM-DD).
|
with the version and date (YYYY-MM-DD).
|
||||||
|
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ go 1.26.0
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
filippo.io/age v1.3.2
|
filippo.io/age v1.3.2
|
||||||
|
git.eeqj.de/sneak/secret v0.0.0-20260810132333-41cea400a7fd
|
||||||
github.com/btcsuite/btcd v0.25.0
|
github.com/btcsuite/btcd v0.25.0
|
||||||
github.com/btcsuite/btcd/btcutil v1.2.0
|
github.com/btcsuite/btcd/btcutil v1.2.0
|
||||||
github.com/spf13/cobra v1.10.2
|
github.com/spf13/cobra v1.10.2
|
||||||
github.com/stretchr/testify v1.12.1
|
github.com/stretchr/testify v1.12.1
|
||||||
|
github.com/tyler-smith/go-bip39 v1.1.0
|
||||||
golang.org/x/crypto v0.57.0
|
golang.org/x/crypto v0.57.0
|
||||||
golang.org/x/term v0.46.0
|
golang.org/x/term v0.46.0
|
||||||
sneak.berlin/go/secret v0.0.0-20261007105610-ef0ae90768c8
|
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
|||||||
@@ -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/age v1.3.2/go.mod h1:TH/Yr2sSRhCKbaH4XPxpUV0Us8Gv6txYUpiZQWz8Evk=
|
||||||
filippo.io/hpke v0.4.0 h1:p575VVQ6ted4pL+it6M00V/f2qTZITO0zgmdKCkd5+A=
|
filippo.io/hpke v0.4.0 h1:p575VVQ6ted4pL+it6M00V/f2qTZITO0zgmdKCkd5+A=
|
||||||
filippo.io/hpke v0.4.0/go.mod h1:EmAN849/P3qdeK+PCMkDpDm83vRHM5cDipBJ8xbQLVY=
|
filippo.io/hpke v0.4.0/go.mod h1:EmAN849/P3qdeK+PCMkDpDm83vRHM5cDipBJ8xbQLVY=
|
||||||
|
git.eeqj.de/sneak/secret v0.0.0-20260810132333-41cea400a7fd h1:6YFV6horz2wDFPWWhour8qx8gLGyO0qoplwEeOuQ2J4=
|
||||||
|
git.eeqj.de/sneak/secret v0.0.0-20260810132333-41cea400a7fd/go.mod h1:gKCcMZvlBOqusn/BxR8IyFmSJQr6R4vvjJ926iNpOSI=
|
||||||
github.com/btcsuite/btcd v0.25.0 h1:JPbjwvHGpSywBRuorFFqTjaVP4y6Qw69XJ1nQ6MyWJM=
|
github.com/btcsuite/btcd v0.25.0 h1:JPbjwvHGpSywBRuorFFqTjaVP4y6Qw69XJ1nQ6MyWJM=
|
||||||
github.com/btcsuite/btcd v0.25.0/go.mod h1:qbPE+pEiR9643E1s1xu57awsRhlCIm1ZIi6FfeRA4KE=
|
github.com/btcsuite/btcd v0.25.0/go.mod h1:qbPE+pEiR9643E1s1xu57awsRhlCIm1ZIi6FfeRA4KE=
|
||||||
github.com/btcsuite/btcd/btcec/v2 v2.5.0 h1:KioMXOWa76b86sTZZOmbzv/ldaQCmB8KFAyn5PbB8E8=
|
github.com/btcsuite/btcd/btcec/v2 v2.5.0 h1:KioMXOWa76b86sTZZOmbzv/ldaQCmB8KFAyn5PbB8E8=
|
||||||
@@ -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/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
|
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
|
||||||
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
|
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
|
||||||
|
github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8=
|
||||||
|
github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U=
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||||
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
||||||
|
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 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
|
||||||
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
|
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 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||||
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||||
golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
|
golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
|
||||||
golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=
|
golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=
|
||||||
|
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=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
sneak.berlin/go/secret v0.0.0-20261007105610-ef0ae90768c8 h1:Bd2bUsRCN8xhcrpH3LpJqHxR2QffeOuF5z76iP4BFnw=
|
|
||||||
sneak.berlin/go/secret v0.0.0-20261007105610-ef0ae90768c8/go.mod h1:uVx9ZuE7ZMIOBY2B1ElXCs97CTsEfuham3NQRdLj5lI=
|
|
||||||
|
|||||||
@@ -1,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"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"git.eeqj.de/sneak/secret/pkg/bip85"
|
||||||
"github.com/btcsuite/btcd/btcutil/hdkeychain"
|
"github.com/btcsuite/btcd/btcutil/hdkeychain"
|
||||||
"sneak.berlin/go/keyfunc/internal/bip39"
|
bip39 "github.com/tyler-smith/go-bip39"
|
||||||
"sneak.berlin/go/keyfunc/internal/derive"
|
"sneak.berlin/go/keyfunc/internal/derive"
|
||||||
"sneak.berlin/go/secret/pkg/bip85"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// english is the number BIP-85 gives the English word list.
|
// english is the number BIP-85 gives the English word list.
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
|
|
||||||
"github.com/btcsuite/btcd/btcutil/hdkeychain"
|
"github.com/btcsuite/btcd/btcutil/hdkeychain"
|
||||||
"github.com/stretchr/testify/require"
|
"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/childmnemonic"
|
||||||
"sneak.berlin/go/keyfunc/internal/derive"
|
"sneak.berlin/go/keyfunc/internal/derive"
|
||||||
)
|
)
|
||||||
|
|||||||
+21
-155
@@ -3,28 +3,17 @@
|
|||||||
package age
|
package age
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"sneak.berlin/go/keyfunc/internal/agekey"
|
"sneak.berlin/go/keyfunc/internal/agekey"
|
||||||
"sneak.berlin/go/keyfunc/internal/cli/options"
|
"sneak.berlin/go/keyfunc/internal/cli/options"
|
||||||
"sneak.berlin/go/keyfunc/internal/cli/signals"
|
|
||||||
"sneak.berlin/go/keyfunc/internal/derive"
|
"sneak.berlin/go/keyfunc/internal/derive"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrInterrupted is returned when SIGINT, SIGTERM or SIGHUP has been
|
|
||||||
// received by the time the work writing the file --output names ends.
|
|
||||||
var ErrInterrupted = errors.New(
|
|
||||||
"interrupted by a signal; the output file was left as it was",
|
|
||||||
)
|
|
||||||
|
|
||||||
// Command returns the age command and everything under it.
|
// Command returns the age command and everything under it.
|
||||||
func Command() *cobra.Command {
|
func Command() *cobra.Command {
|
||||||
group := &cobra.Command{
|
group := &cobra.Command{
|
||||||
@@ -139,12 +128,8 @@ func runDecrypt(cmd *cobra.Command, args []string) error {
|
|||||||
return through(cmd, args, key.Decrypt)
|
return through(cmd, args, key.Decrypt)
|
||||||
}
|
}
|
||||||
|
|
||||||
// through opens the input the arguments ask for and hands it to the
|
// through opens the input and the output the arguments ask for, hands
|
||||||
// work, with the file --output names to write to, or the command's own
|
// them to the work, and finishes the output afterwards either way.
|
||||||
// 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(
|
func through(
|
||||||
cmd *cobra.Command, args []string,
|
cmd *cobra.Command, args []string,
|
||||||
work func(io.Writer, io.Reader) error,
|
work func(io.Writer, io.Reader) error,
|
||||||
@@ -156,41 +141,14 @@ func through(
|
|||||||
|
|
||||||
defer closeSrc()
|
defer closeSrc()
|
||||||
|
|
||||||
name, err := cmd.Flags().GetString("output")
|
dst, done, err := output(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("reading the output file: %w", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
switch {
|
err = work(dst, src)
|
||||||
case name == "", same(name, cmd.OutOrStdout()):
|
|
||||||
return work(cmd.OutOrStdout(), src)
|
|
||||||
case same(name, cmd.ErrOrStderr()):
|
|
||||||
return work(cmd.ErrOrStderr(), src)
|
|
||||||
default:
|
|
||||||
return output(name, src, work)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// same reports whether the named path, followed to the end, is the
|
return done(err)
|
||||||
// file the stream writes to, whatever name it is reached by, such as
|
|
||||||
// /dev/stdout or /dev/fd/1 for standard output.
|
|
||||||
func same(name string, stream io.Writer) bool {
|
|
||||||
file, ok := stream.(*os.File)
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
streamInfo, err := file.Stat()
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
info, err := os.Stat(name)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return os.SameFile(info, streamInfo)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// input returns what to read from: the named file, or the command's
|
// input returns what to read from: the named file, or the command's
|
||||||
@@ -209,127 +167,35 @@ func input(cmd *cobra.Command, args []string) (io.Reader, func(), error) {
|
|||||||
return file, func() { _ = file.Close() }, nil
|
return file, func() { _ = file.Close() }, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// output has the work write to the named path, going by what is there
|
// output returns what to write to: a new file beside the one --output
|
||||||
// without following a final symlink:
|
// names, or the command's own output when it names none. The second
|
||||||
//
|
// result finishes the write, and is given whatever the work returned:
|
||||||
// - nothing, or a regular file: replace writes a new file beside it
|
// the new file takes the named file's place only when the work
|
||||||
// and renames that over it;
|
// succeeded, so a file that is already there survives a run that
|
||||||
// - a symlink: the same for what it points at, so that the link keeps
|
// failed.
|
||||||
// pointing where it did; one that points at nothing is refused;
|
func output(cmd *cobra.Command) (io.Writer, func(error) error, error) {
|
||||||
// - anything else, such as a named pipe or a device like /dev/null:
|
name, err := cmd.Flags().GetString("output")
|
||||||
// direct writes to it, since a rename would put a regular file in
|
|
||||||
// its place.
|
|
||||||
func output(
|
|
||||||
name string, src io.Reader, work func(io.Writer, io.Reader) error,
|
|
||||||
) error {
|
|
||||||
info, err := os.Lstat(name)
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case errors.Is(err, fs.ErrNotExist):
|
|
||||||
return replace(name, src, work)
|
|
||||||
case err != nil:
|
|
||||||
return fmt.Errorf("looking at %s: %w", name, err)
|
|
||||||
case info.Mode().IsRegular():
|
|
||||||
return replace(name, src, work)
|
|
||||||
case info.Mode().Type() == fs.ModeSymlink:
|
|
||||||
// os.Stat follows the link as opening it would. /dev/fd/3
|
|
||||||
// needs that: it reaches a pipe or a terminal through a link
|
|
||||||
// that names no path.
|
|
||||||
info, err = os.Stat(name)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("following %s: %w", name, err)
|
return nil, nil, fmt.Errorf("reading the output file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !info.Mode().IsRegular() {
|
if name == "" {
|
||||||
return direct(name, src, work)
|
return cmd.OutOrStdout(), func(failed error) error {
|
||||||
}
|
|
||||||
|
|
||||||
target, err := filepath.EvalSymlinks(name)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("following %s: %w", name, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return replace(target, src, work)
|
|
||||||
default:
|
|
||||||
return direct(name, src, work)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// direct has the work write straight to the named path, which is there
|
|
||||||
// and is not a regular file. No signal is caught, so one ends the tool
|
|
||||||
// as it ends any other command.
|
|
||||||
func direct(
|
|
||||||
name string, src io.Reader, work func(io.Writer, io.Reader) error,
|
|
||||||
) error {
|
|
||||||
file, err := os.OpenFile(name, os.O_WRONLY, 0) //nolint:gosec // the -o path
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("opening %s: %w", name, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
failed := work(file, src)
|
|
||||||
closeErr := file.Close()
|
|
||||||
|
|
||||||
if failed != nil {
|
|
||||||
return failed
|
return failed
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if closeErr != nil {
|
|
||||||
return fmt.Errorf("finishing %s: %w", name, closeErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// replace has the work write a new file beside the named one, and puts
|
|
||||||
// the new file in the named file's place only when the work succeeded,
|
|
||||||
// so a file that is already there survives a run that failed.
|
|
||||||
//
|
|
||||||
// Meanwhile SIGINT, SIGTERM and SIGHUP are caught, as signals.Context
|
|
||||||
// does. One the tool has received by the time the work ends wins: the
|
|
||||||
// new file is removed and ErrInterrupted returned, at once if the work
|
|
||||||
// is still running, without waiting for it, since it may be blocked
|
|
||||||
// reading its input.
|
|
||||||
func replace(
|
|
||||||
name string, src io.Reader, work func(io.Writer, io.Reader) error,
|
|
||||||
) error {
|
|
||||||
// received is registered before the context, so it gets every
|
|
||||||
// signal the context gets.
|
|
||||||
received := make(chan os.Signal, 1)
|
|
||||||
signals.Notify(received)
|
|
||||||
|
|
||||||
defer signal.Stop(received)
|
|
||||||
|
|
||||||
// The context goes on catching the signals until the file is in
|
|
||||||
// place or removed, so that a later one cannot end the tool with
|
|
||||||
// the new file left beside the named one.
|
|
||||||
interrupted, stop := signals.Context(context.Background())
|
|
||||||
defer stop()
|
|
||||||
|
|
||||||
// The file is made in the same directory so that putting it in
|
// The file is made in the same directory so that putting it in
|
||||||
// place is a rename and never a copy, and it is readable only by
|
// place is a rename and never a copy, and it is readable only by
|
||||||
// its owner, which is the mode it keeps once renamed.
|
// its owner, which is the mode it keeps once renamed.
|
||||||
file, err := os.CreateTemp(filepath.Dir(name), filepath.Base(name)+".")
|
file, err := os.CreateTemp(filepath.Dir(name), filepath.Base(name)+".")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("creating a file beside %s: %w", name, err)
|
return nil, nil, fmt.Errorf("creating a file beside %s: %w", name, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
worked := make(chan error, 1)
|
return file, func(failed error) error {
|
||||||
|
|
||||||
go func() { worked <- work(file, src) }()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case failed := <-worked:
|
|
||||||
// Stop returns only once every signal the tool has received
|
|
||||||
// has been handed over, so an empty received means none came.
|
|
||||||
signal.Stop(received)
|
|
||||||
|
|
||||||
if len(received) == 0 {
|
|
||||||
return finish(file, name, failed)
|
return finish(file, name, failed)
|
||||||
}
|
}, nil
|
||||||
case <-interrupted.Done():
|
|
||||||
}
|
|
||||||
|
|
||||||
return finish(file, name, ErrInterrupted)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// finish closes the new file and puts it in the named file's place, or
|
// finish closes the new file and puts it in the named file's place, or
|
||||||
|
|||||||
@@ -1,22 +1,13 @@
|
|||||||
package cli_test
|
package cli_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"io/fs"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"os/signal"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"sneak.berlin/go/keyfunc/internal/agekey"
|
"sneak.berlin/go/keyfunc/internal/agekey"
|
||||||
"sneak.berlin/go/keyfunc/internal/cli"
|
|
||||||
"sneak.berlin/go/keyfunc/internal/cli/age"
|
|
||||||
"sneak.berlin/go/keyfunc/internal/mnemonic"
|
"sneak.berlin/go/keyfunc/internal/mnemonic"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -99,360 +90,6 @@ func TestARefusedDecryptionLeavesTheOutputFileAlone(t *testing.T) {
|
|||||||
require.Equal(t, "what was already there\n", string(kept))
|
require.Equal(t, "what was already there\n", string(kept))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestASymlinkAtTheOutputPathStaysAndItsTargetGetsTheOutput(t *testing.T) {
|
|
||||||
t.Setenv(mnemonic.Variable, example())
|
|
||||||
|
|
||||||
plain := written(t, "notes.txt", "the secret\n")
|
|
||||||
target := written(t, "notes.age", "what was already there\n")
|
|
||||||
link := filepath.Join(t.TempDir(), "notes.age")
|
|
||||||
require.NoError(t, os.Symlink(target, link))
|
|
||||||
|
|
||||||
run(t, "age", "encrypt", "-o", link, plain)
|
|
||||||
|
|
||||||
pointsAt, err := os.Readlink(link)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, target, pointsAt)
|
|
||||||
require.Equal(t, "the secret\n", run(t, "age", "decrypt", target))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestANamedPipeAtTheOutputPathIsWrittenToAndStaysAPipe(t *testing.T) {
|
|
||||||
t.Setenv(mnemonic.Variable, example())
|
|
||||||
|
|
||||||
plain := written(t, "notes.txt", "the secret\n")
|
|
||||||
pipe := filepath.Join(t.TempDir(), "notes.age")
|
|
||||||
require.NoError(t, syscall.Mkfifo(pipe, fileMode))
|
|
||||||
|
|
||||||
// Opening the pipe to read waits until the tool opens it to write.
|
|
||||||
var sealed []byte
|
|
||||||
|
|
||||||
finished := make(chan error, 1)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
sealed, err = os.ReadFile(pipe) //nolint:gosec // the test's own path
|
|
||||||
finished <- err
|
|
||||||
}()
|
|
||||||
|
|
||||||
run(t, "age", "encrypt", "-o", pipe, plain)
|
|
||||||
|
|
||||||
select {
|
|
||||||
case err := <-finished:
|
|
||||||
require.NoError(t, err)
|
|
||||||
case <-time.After(5 * time.Second):
|
|
||||||
t.Fatal("nothing was written to the pipe")
|
|
||||||
}
|
|
||||||
|
|
||||||
info, err := os.Lstat(pipe)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, fs.ModeNamedPipe, info.Mode().Type())
|
|
||||||
|
|
||||||
sealedFile := written(t, "notes.age", string(sealed))
|
|
||||||
require.Equal(t, "the secret\n", run(t, "age", "decrypt", sealedFile))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestANameForStandardOutputAddsToTheFileItIsAppendedTo(t *testing.T) {
|
|
||||||
t.Setenv(mnemonic.Variable, example())
|
|
||||||
|
|
||||||
sealed := filepath.Join(t.TempDir(), "notes.age")
|
|
||||||
run(t, "age", "encrypt", "-o", sealed, written(t, "notes.txt", "the secret\n"))
|
|
||||||
|
|
||||||
for _, name := range []string{"/dev/stdout", "/dev/fd/1"} {
|
|
||||||
appendedThrough(t, name, sealed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// appendedThrough decrypts sealed with -o name while the tool's standard
|
|
||||||
// output is appended to a file that already has contents, as the shell's
|
|
||||||
// ">> notes.out" does, and checks that the file is the same one, with
|
|
||||||
// the same mode, and holds its earlier contents and then the output.
|
|
||||||
func appendedThrough(t *testing.T, name, sealed string) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
// A mode of its own, so that a replaced file would show.
|
|
||||||
const ownMode = 0o644
|
|
||||||
|
|
||||||
existing := written(t, "notes.out", "what was already there\n")
|
|
||||||
require.NoError(t, os.Chmod(existing, ownMode))
|
|
||||||
|
|
||||||
before, err := os.Stat(existing)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
//nolint:gosec // the test made this path itself
|
|
||||||
appended, err := os.OpenFile(existing, os.O_WRONLY|os.O_APPEND, 0)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
defer func() { _ = appended.Close() }()
|
|
||||||
|
|
||||||
//nolint:gosec // this test's own binary as the tool
|
|
||||||
command := exec.CommandContext(
|
|
||||||
t.Context(), os.Args[0], "age", "decrypt", "-o", name, sealed,
|
|
||||||
)
|
|
||||||
|
|
||||||
command.Env = append(os.Environ(), runAsTool+"=1")
|
|
||||||
command.Stdout = appended
|
|
||||||
|
|
||||||
require.NoError(t, command.Run(), name)
|
|
||||||
require.Equal(t,
|
|
||||||
"what was already there\nthe secret\n", read(t, existing), name,
|
|
||||||
)
|
|
||||||
|
|
||||||
after, err := os.Stat(existing)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.True(t, os.SameFile(before, after), name)
|
|
||||||
require.Equal(t, os.FileMode(ownMode), after.Mode().Perm(), name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestASignalStopsAnEncryptionAndLeavesNoFile(t *testing.T) {
|
|
||||||
t.Setenv(mnemonic.Variable, example())
|
|
||||||
|
|
||||||
for _, ending := range []os.Signal{
|
|
||||||
syscall.SIGTERM, syscall.SIGINT, syscall.SIGHUP,
|
|
||||||
} {
|
|
||||||
interrupted(t, ending, "encrypt", "the start of the secret\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestASignalStopsADecryptionAndLeavesNoFile(t *testing.T) {
|
|
||||||
t.Setenv(mnemonic.Variable, example())
|
|
||||||
|
|
||||||
// All of an encryption but its last byte, so the tool reads the
|
|
||||||
// header and then waits for the rest.
|
|
||||||
sealed := run(t, "age", "encrypt", written(t, "notes.txt", "the secret\n"))
|
|
||||||
cut := sealed[:len(sealed)-1]
|
|
||||||
|
|
||||||
for _, ending := range []os.Signal{
|
|
||||||
syscall.SIGTERM, syscall.SIGINT, syscall.SIGHUP,
|
|
||||||
} {
|
|
||||||
interrupted(t, ending, "decrypt", cut)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestASignalReceivedAsTheInputEndsLeavesTheFileAsItWas(t *testing.T) {
|
|
||||||
t.Setenv(mnemonic.Variable, example())
|
|
||||||
|
|
||||||
sealed := run(t, "age", "encrypt", written(t, "notes.txt", "the secret\n"))
|
|
||||||
|
|
||||||
for _, ending := range []syscall.Signal{
|
|
||||||
syscall.SIGTERM, syscall.SIGINT, syscall.SIGHUP,
|
|
||||||
} {
|
|
||||||
receivedAtTheEnd(t, ending, "encrypt", "the secret\n")
|
|
||||||
receivedAtTheEnd(t, ending, "decrypt", sealed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestASignalAsTheInputEndsLeavesNoUnfinishedFile(t *testing.T) {
|
|
||||||
t.Setenv(mnemonic.Variable, example())
|
|
||||||
|
|
||||||
sealed := run(t, "age", "encrypt", written(t, "notes.txt", "the secret\n"))
|
|
||||||
|
|
||||||
// Ctrl-C on "producer | keyfunc age encrypt -o file" ends the
|
|
||||||
// producer too, so the input ends just as the signal comes, with
|
|
||||||
// enough of it in hand for a whole encryption or decryption. Which
|
|
||||||
// of the two the tool has first varies, so it is tried often, and
|
|
||||||
// a whole file in place is accepted as well as none.
|
|
||||||
for range 25 {
|
|
||||||
named := signalledAsTheInputEnds(t, "encrypt", "the start of the secret\n")
|
|
||||||
if named != "" {
|
|
||||||
require.Equal(t,
|
|
||||||
"the start of the secret\n", run(t, "age", "decrypt", named),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
named = signalledAsTheInputEnds(t, "decrypt", sealed)
|
|
||||||
if named != "" {
|
|
||||||
require.Equal(t, "the secret\n", read(t, named))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAnEncryptionStartedUnderNohupSurvivesAHangup(t *testing.T) {
|
|
||||||
t.Setenv(mnemonic.Variable, example())
|
|
||||||
|
|
||||||
directory := t.TempDir()
|
|
||||||
named := filepath.Join(directory, "notes")
|
|
||||||
|
|
||||||
// nohup starts the tool with SIGHUP ignored. A tool that caught it
|
|
||||||
// anyway would turn it back on and be ended by it.
|
|
||||||
command, producer := writing(
|
|
||||||
t, directory, "the secret\n",
|
|
||||||
"nohup", os.Args[0], "age", "encrypt", "-o", named,
|
|
||||||
)
|
|
||||||
|
|
||||||
require.NoError(t, command.Process.Signal(syscall.SIGHUP))
|
|
||||||
require.NoError(t, producer.Close())
|
|
||||||
waitForTool(t, "SIGHUP under nohup", command)
|
|
||||||
|
|
||||||
require.Equal(t, 0, command.ProcessState.ExitCode())
|
|
||||||
|
|
||||||
left, err := os.ReadDir(directory)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Len(t, left, 1)
|
|
||||||
require.Equal(t, "the secret\n", run(t, "age", "decrypt", named))
|
|
||||||
}
|
|
||||||
|
|
||||||
// interrupted runs "age encrypt -o" or "age decrypt -o", as the
|
|
||||||
// operation says, writing into a directory of its own, and once it has
|
|
||||||
// begun writing sends it the signal and leaves the input open. The tool
|
|
||||||
// has to end with status 1 and leave the directory empty. A tool that
|
|
||||||
// went on reading would not end until the input did; one that did not
|
|
||||||
// remove the file it was writing would leave it there, with what it had
|
|
||||||
// written so far.
|
|
||||||
func interrupted(t *testing.T, ending os.Signal, operation, input string) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
name := operation + " " + ending.String()
|
|
||||||
directory := t.TempDir()
|
|
||||||
|
|
||||||
command, _ := writing(
|
|
||||||
t, directory, input,
|
|
||||||
os.Args[0], "age", operation, "-o", filepath.Join(directory, "notes"),
|
|
||||||
)
|
|
||||||
|
|
||||||
require.NoError(t, command.Process.Signal(ending))
|
|
||||||
waitForTool(t, name, command)
|
|
||||||
|
|
||||||
require.Equal(t, 1, command.ProcessState.ExitCode(), name)
|
|
||||||
|
|
||||||
left, err := os.ReadDir(directory)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Empty(t, left, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// signalledAsTheInputEnds runs "age encrypt -o" or "age decrypt -o", as
|
|
||||||
// the operation says, writing into a directory of its own, and once it
|
|
||||||
// has begun writing sends it SIGINT and at once ends its input. Either
|
|
||||||
// the tool ends with status 1 and leaves the directory empty, and ""
|
|
||||||
// is returned, or it ends otherwise and leaves only the named file,
|
|
||||||
// whose path is returned for the caller to check that it is whole.
|
|
||||||
func signalledAsTheInputEnds(t *testing.T, operation, input string) string {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
directory := t.TempDir()
|
|
||||||
named := filepath.Join(directory, "notes")
|
|
||||||
|
|
||||||
command, producer := writing(
|
|
||||||
t, directory, input, os.Args[0], "age", operation, "-o", named,
|
|
||||||
)
|
|
||||||
|
|
||||||
require.NoError(t, command.Process.Signal(syscall.SIGINT))
|
|
||||||
require.NoError(t, producer.Close())
|
|
||||||
waitForTool(t, operation, command)
|
|
||||||
|
|
||||||
left, err := os.ReadDir(directory)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
if command.ProcessState.ExitCode() == failedStatus {
|
|
||||||
require.Empty(t, left, operation)
|
|
||||||
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
require.Len(t, left, 1, operation)
|
|
||||||
|
|
||||||
return named
|
|
||||||
}
|
|
||||||
|
|
||||||
// receivedAtTheEnd runs "age encrypt -o" or "age decrypt -o", as the
|
|
||||||
// operation says, in this process, over a file that is already there,
|
|
||||||
// with an input that at its end sends this process the signal and waits
|
|
||||||
// until it has been received. The tool has to return ErrInterrupted and
|
|
||||||
// leave that file as it was, with nothing beside it. A tool that went
|
|
||||||
// by the end of the input alone would put its new file in place.
|
|
||||||
func receivedAtTheEnd(
|
|
||||||
t *testing.T, ending syscall.Signal, operation, input string,
|
|
||||||
) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
name := operation + " " + ending.String()
|
|
||||||
existing := written(t, "notes", "what was already there\n")
|
|
||||||
|
|
||||||
// The test catches the signal as well, so that it does not end the
|
|
||||||
// test binary and so that the input can wait for it.
|
|
||||||
received := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(received, ending)
|
|
||||||
|
|
||||||
defer signal.Stop(received)
|
|
||||||
|
|
||||||
root := cli.Root()
|
|
||||||
root.SetIn(&endingInASignal{
|
|
||||||
rest: strings.NewReader(input), ending: ending, received: received,
|
|
||||||
})
|
|
||||||
root.SetOut(io.Discard)
|
|
||||||
root.SetErr(io.Discard)
|
|
||||||
root.SetArgs([]string{"age", operation, "-o", existing})
|
|
||||||
|
|
||||||
err := root.ExecuteContext(t.Context())
|
|
||||||
require.ErrorIs(t, err, age.ErrInterrupted, name)
|
|
||||||
|
|
||||||
require.Equal(t, "what was already there\n", read(t, existing), name)
|
|
||||||
|
|
||||||
left, err := os.ReadDir(filepath.Dir(existing))
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Len(t, left, 1, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// endingInASignal is an input that, when it runs out, sends this
|
|
||||||
// process its signal and waits for it on received before it reports its
|
|
||||||
// end. It sends the signal only once: once nothing catches it, another
|
|
||||||
// would end the test binary.
|
|
||||||
type endingInASignal struct {
|
|
||||||
rest io.Reader
|
|
||||||
ending syscall.Signal
|
|
||||||
received chan os.Signal
|
|
||||||
sent bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (input *endingInASignal) Read(buffer []byte) (int, error) {
|
|
||||||
n, err := input.rest.Read(buffer)
|
|
||||||
if !errors.Is(err, io.EOF) || input.sent {
|
|
||||||
return n, err
|
|
||||||
}
|
|
||||||
|
|
||||||
input.sent = true
|
|
||||||
|
|
||||||
err = syscall.Kill(os.Getpid(), input.ending)
|
|
||||||
if err != nil {
|
|
||||||
return n, err
|
|
||||||
}
|
|
||||||
|
|
||||||
<-input.received
|
|
||||||
|
|
||||||
return n, io.EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
// writing starts argv, the tool told to write into directory, as a
|
|
||||||
// subprocess reading the input from a pipe, and returns once the tool
|
|
||||||
// has begun writing the file beside the one it was named. The pipe is
|
|
||||||
// left open for the caller to end.
|
|
||||||
func writing(
|
|
||||||
t *testing.T, directory, input string, argv ...string,
|
|
||||||
) (*exec.Cmd, io.WriteCloser) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
//nolint:gosec // this test's own binary as the tool, or nohup running it
|
|
||||||
command := exec.CommandContext(t.Context(), argv[0], argv[1:]...)
|
|
||||||
|
|
||||||
command.Env = append(os.Environ(), runAsTool+"=1")
|
|
||||||
|
|
||||||
producer, err := command.StdinPipe()
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NoError(t, command.Start())
|
|
||||||
|
|
||||||
_, err = io.WriteString(producer, input)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// The file beside the named one is made once the mnemonic has been
|
|
||||||
// read, before any input is.
|
|
||||||
require.Eventually(t, func() bool {
|
|
||||||
entries, err := os.ReadDir(directory)
|
|
||||||
|
|
||||||
return err == nil && len(entries) > 0
|
|
||||||
}, 5*time.Second, 5*time.Millisecond)
|
|
||||||
|
|
||||||
return command, producer
|
|
||||||
}
|
|
||||||
|
|
||||||
// written puts the contents in a file of that name in a directory of
|
// written puts the contents in a file of that name in a directory of
|
||||||
// this test's own and returns the path to it.
|
// this test's own and returns the path to it.
|
||||||
func written(t *testing.T, name, contents string) string {
|
func written(t *testing.T, name, contents string) string {
|
||||||
|
|||||||
+14
-21
@@ -2,11 +2,13 @@
|
|||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"os/signal"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"sneak.berlin/go/keyfunc/internal/cli/age"
|
"sneak.berlin/go/keyfunc/internal/cli/age"
|
||||||
@@ -62,33 +64,24 @@ func Root() *cobra.Command {
|
|||||||
return root
|
return root
|
||||||
}
|
}
|
||||||
|
|
||||||
// init keeps the command on the main thread. Linux hands a signal sent
|
|
||||||
// to the tool to that thread first, and a thread runs a pending signal
|
|
||||||
// handler before its own code, so when "age encrypt -o" or "age
|
|
||||||
// decrypt -o" checks for a signal as its input ends, one sent before
|
|
||||||
// then, as by Ctrl-C on a pipeline, has been received.
|
|
||||||
//
|
|
||||||
//nolint:gochecknoinits // only an init can keep main on the main thread
|
|
||||||
func init() {
|
|
||||||
runtime.LockOSThread()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Main runs the tool and returns the status the process should exit
|
// Main runs the tool and returns the status the process should exit
|
||||||
// with. An error ends the tool with status 1, except when it carries a
|
// with. An error ends the tool with status 1, except when it carries a
|
||||||
// status of its own, which "ssh to" uses to hand on the status ssh
|
// status of its own, which "ssh to" uses to hand on the status ssh
|
||||||
// ended with. ssh has already said whatever it had to say in that
|
// ended with. ssh has already said whatever it had to say in that
|
||||||
// case, so nothing more is printed.
|
// case, so nothing more is printed.
|
||||||
//
|
//
|
||||||
// SIGINT, SIGTERM and SIGHUP end the tool at once, as they end any Go
|
// SIGINT, SIGTERM and SIGHUP cancel the command's context instead of
|
||||||
// program, so a command waiting at the mnemonic prompt or reading what
|
// killing the process outright, so the child ssh or sftp ends and the
|
||||||
// it encrypts or decrypts goes no further. The exceptions catch the
|
// deferred cleanup that removes the agent socket and the install
|
||||||
// signals to clean up first: "ssh to" and "ssh install" while they
|
// working directory still runs.
|
||||||
// have ssh or sftp running, so the child ends and their own cleanup
|
|
||||||
// still runs, and "age encrypt -o" and "age decrypt -o" while they
|
|
||||||
// write a new file to rename over the named one, so the unfinished file
|
|
||||||
// is removed.
|
|
||||||
func Main() int {
|
func Main() int {
|
||||||
err := Root().Execute()
|
ctx, stop := signal.NotifyContext(
|
||||||
|
context.Background(),
|
||||||
|
syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP,
|
||||||
|
)
|
||||||
|
defer stop()
|
||||||
|
|
||||||
|
err := Root().ExecuteContext(ctx)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
bip39 "github.com/tyler-smith/go-bip39"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
"sneak.berlin/go/keyfunc/internal/bip39"
|
|
||||||
"sneak.berlin/go/keyfunc/internal/childmnemonic"
|
"sneak.berlin/go/keyfunc/internal/childmnemonic"
|
||||||
"sneak.berlin/go/keyfunc/internal/cli"
|
"sneak.berlin/go/keyfunc/internal/cli"
|
||||||
"sneak.berlin/go/keyfunc/internal/derive"
|
"sneak.berlin/go/keyfunc/internal/derive"
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
// Package signals catches the signals that end the tool, for the
|
|
||||||
// commands that clean up before they end.
|
|
||||||
package signals
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"syscall"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Context is signal.NotifyContext for SIGINT, SIGTERM and SIGHUP: the
|
|
||||||
// context it returns is cancelled when one of them arrives, and stop
|
|
||||||
// stops catching them. It leaves out any of the three the tool was
|
|
||||||
// started with set to be ignored, as nohup does with SIGHUP, because
|
|
||||||
// catching a signal turns an ignored one back on and would end a run
|
|
||||||
// that was meant to survive it.
|
|
||||||
func Context(parent context.Context) (context.Context, context.CancelFunc) {
|
|
||||||
endings := caught()
|
|
||||||
|
|
||||||
// Given no signals at all, NotifyContext would catch every one.
|
|
||||||
if len(endings) == 0 {
|
|
||||||
return context.WithCancel(parent)
|
|
||||||
}
|
|
||||||
|
|
||||||
return signal.NotifyContext(parent, endings...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Notify is signal.Notify for the signals Context catches: each one
|
|
||||||
// that arrives is sent to c, until signal.Stop(c).
|
|
||||||
func Notify(c chan<- os.Signal) {
|
|
||||||
endings := caught()
|
|
||||||
|
|
||||||
// Given no signals at all, Notify would catch every one.
|
|
||||||
if len(endings) > 0 {
|
|
||||||
signal.Notify(c, endings...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// caught returns those of SIGINT, SIGTERM and SIGHUP that the tool was
|
|
||||||
// not started with set to be ignored.
|
|
||||||
func caught() []os.Signal {
|
|
||||||
endings := []os.Signal{syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP}
|
|
||||||
kept := make([]os.Signal, 0, len(endings))
|
|
||||||
|
|
||||||
for _, ending := range endings {
|
|
||||||
if !signal.Ignored(ending) {
|
|
||||||
kept = append(kept, ending)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return kept
|
|
||||||
}
|
|
||||||
@@ -11,11 +11,8 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"sneak.berlin/go/keyfunc/internal/cli/signals"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Where the key goes on the host and what the file it arrives in is
|
// Where the key goes on the host and what the file it arrives in is
|
||||||
@@ -36,33 +33,12 @@ const (
|
|||||||
localMode = 0o600
|
localMode = 0o600
|
||||||
)
|
)
|
||||||
|
|
||||||
// waitDelay is the WaitDelay sftp runs with: from a signal, or from sftp
|
|
||||||
// ending, how long the tool waits for sftp to end and its output to
|
|
||||||
// close before it kills sftp and stops reading. That is ample for sftp
|
|
||||||
// to stop the ssh it started, and short enough that a signal still ends
|
|
||||||
// the tool within a second.
|
|
||||||
const waitDelay = 250 * time.Millisecond
|
|
||||||
|
|
||||||
// ErrCannotEnter is the refusal of a host whose .ssh is there but
|
// ErrCannotEnter is the refusal of a host whose .ssh is there but
|
||||||
// cannot be entered, so that nothing in it can be read or written.
|
// cannot be entered, so that nothing in it can be read or written.
|
||||||
var ErrCannotEnter = errors.New(
|
var ErrCannotEnter = errors.New(
|
||||||
"~/.ssh is there on the host but cannot be entered",
|
"~/.ssh is there on the host but cannot be entered",
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrSymlink is the refusal of a host whose authorized_keys is a
|
|
||||||
// symlink: the rename that puts the new file in place would replace the
|
|
||||||
// link itself, and the file it points at would never get the key.
|
|
||||||
var ErrSymlink = errors.New(
|
|
||||||
"~/.ssh/authorized_keys on the host is a symlink, which the tool " +
|
|
||||||
"leaves alone",
|
|
||||||
)
|
|
||||||
|
|
||||||
// ErrStrayArgument is the refusal of anything but the host before --,
|
|
||||||
// which would otherwise be handed to sftp in front of the host.
|
|
||||||
var ErrStrayArgument = errors.New(
|
|
||||||
"only the host goes before --; options for sftp go after --",
|
|
||||||
)
|
|
||||||
|
|
||||||
// install returns the command that adds the public key to a host.
|
// install returns the command that adds the public key to a host.
|
||||||
func install() *cobra.Command {
|
func install() *cobra.Command {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
@@ -74,22 +50,7 @@ func install() *cobra.Command {
|
|||||||
"beside it which is then renamed over it. Nothing is run " +
|
"beside it which is then renamed over it. Nothing is run " +
|
||||||
"on the host. Anything after -- is given to sftp " +
|
"on the host. Anything after -- is given to sftp " +
|
||||||
"unchanged, which is where the port goes (-P).",
|
"unchanged, which is where the port goes (-P).",
|
||||||
Args: cobra.MatchAll(
|
Args: cobra.MinimumNArgs(1),
|
||||||
cobra.MinimumNArgs(1),
|
|
||||||
func(cmd *cobra.Command, args []string) error {
|
|
||||||
// ArgsLenAtDash is -1 when there is no --.
|
|
||||||
before := cmd.ArgsLenAtDash()
|
|
||||||
if before == -1 {
|
|
||||||
before = len(args)
|
|
||||||
}
|
|
||||||
|
|
||||||
if before != 1 {
|
|
||||||
return ErrStrayArgument
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
),
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
key, comment, err := derived(cmd)
|
key, comment, err := derived(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -101,14 +62,6 @@ func install() *cobra.Command {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// From here on a signal cancels the context, which
|
|
||||||
// sftp runs under, instead of ending the tool, so sftp
|
|
||||||
// ends and the working directory is still removed.
|
|
||||||
ctx, stop := signals.Context(cmd.Context())
|
|
||||||
defer stop()
|
|
||||||
|
|
||||||
cmd.SetContext(ctx)
|
|
||||||
|
|
||||||
return add(cmd, args[0], args[1:], line)
|
return add(cmd, args[0], args[1:], line)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -225,24 +178,8 @@ func session(
|
|||||||
command.Stdout = &said
|
command.Stdout = &said
|
||||||
command.Stderr = &said
|
command.Stderr = &said
|
||||||
|
|
||||||
// A cancelled context means a signal arrived. Send sftp a SIGTERM
|
|
||||||
// rather than the default kill, so it stops the ssh it started
|
|
||||||
// before it goes. Anything sftp started that still holds its output
|
|
||||||
// keeps the tool waiting no longer than waitDelay.
|
|
||||||
command.Cancel = func() error {
|
|
||||||
return command.Process.Signal(syscall.SIGTERM)
|
|
||||||
}
|
|
||||||
command.WaitDelay = waitDelay
|
|
||||||
|
|
||||||
err := command.Run()
|
err := command.Run()
|
||||||
|
|
||||||
// sftp ended well and only something it started, such as the
|
|
||||||
// master ssh leaves running for ControlPersist under -v, still held
|
|
||||||
// its output: the session worked.
|
|
||||||
if errors.Is(err, exec.ErrWaitDelay) {
|
|
||||||
err = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
_, _ = cmd.ErrOrStderr().Write(said.Bytes())
|
_, _ = cmd.ErrOrStderr().Write(said.Bytes())
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -285,23 +222,14 @@ func merge(content, line string) (string, bool) {
|
|||||||
// it can be looked up, not even ".". The first listing of such a
|
// it can be looked up, not even ".". The first listing of such a
|
||||||
// directory comes up empty, as the server leaves out every name it
|
// directory comes up empty, as the server leaves out every name it
|
||||||
// cannot look up.
|
// cannot look up.
|
||||||
//
|
|
||||||
// The first listing is a long one, which shows an authorized_keys that
|
|
||||||
// is a symlink as one. That is refused before anything else sftp said
|
|
||||||
// is read, so a link the get could not follow is refused in the same
|
|
||||||
// words.
|
|
||||||
func fetch(
|
func fetch(
|
||||||
cmd *cobra.Command, host string, options []string, into string,
|
cmd *cobra.Command, host string, options []string, into string,
|
||||||
) (string, bool, error) {
|
) (string, bool, error) {
|
||||||
said, err := session(cmd, host, options, []string{
|
said, err := session(cmd, host, options, []string{
|
||||||
"ls -n " + directory,
|
"ls -1 " + directory,
|
||||||
"ls -1 " + directory + "/.",
|
"ls -1 " + directory + "/.",
|
||||||
"get " + authorized + " " + quoted(into),
|
"get " + authorized + " " + quoted(into),
|
||||||
})
|
})
|
||||||
if symlinked(said) {
|
|
||||||
return "", false, ErrSymlink
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if listingNotFound(said, directory) {
|
if listingNotFound(said, directory) {
|
||||||
return "", false, nil
|
return "", false, nil
|
||||||
@@ -364,22 +292,6 @@ func reportedCannotList(line string) (string, bool) {
|
|||||||
return strings.TrimSuffix(strings.TrimPrefix(line, before), after), true
|
return strings.TrimSuffix(strings.TrimPrefix(line, before), after), true
|
||||||
}
|
}
|
||||||
|
|
||||||
// symlinked says whether the long listing of .ssh shows authorized_keys
|
|
||||||
// as a symlink. With -n the client writes each line itself, as ls -l
|
|
||||||
// does, whatever the server: the type comes first, "l" for a symlink,
|
|
||||||
// and the path as the listing named it comes last.
|
|
||||||
func symlinked(said string) bool {
|
|
||||||
for line := range strings.Lines(said) {
|
|
||||||
fields := strings.Fields(line)
|
|
||||||
if len(fields) > 0 && strings.HasPrefix(fields[0], "l") &&
|
|
||||||
fields[len(fields)-1] == authorized {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// absent says whether sftp reported the file that was asked for as
|
// absent says whether sftp reported the file that was asked for as
|
||||||
// not being there, which is the one failure of the fetch that is read
|
// not being there, which is the one failure of the fetch that is read
|
||||||
// as an empty authorized_keys. The reading is taken only from the
|
// as an empty authorized_keys. The reading is taken only from the
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import "testing"
|
|||||||
const (
|
const (
|
||||||
echoed = `sftp> get .ssh/authorized_keys "/tmp/keyfunc/authorized_keys"
|
echoed = `sftp> get .ssh/authorized_keys "/tmp/keyfunc/authorized_keys"
|
||||||
`
|
`
|
||||||
listed = "sftp> ls -n .ssh\n"
|
listed = "sftp> ls -1 .ssh\n"
|
||||||
warning = `Warning: Identity file /gone not accessible: ` +
|
warning = `Warning: Identity file /gone not accessible: ` +
|
||||||
"No such file or directory.\n"
|
"No such file or directory.\n"
|
||||||
)
|
)
|
||||||
|
|||||||
+3
-11
@@ -10,7 +10,6 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"sneak.berlin/go/keyfunc/internal/cli/signals"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// StatusError says the tool should end with the status ssh ended with.
|
// StatusError says the tool should end with the status ssh ended with.
|
||||||
@@ -43,14 +42,7 @@ func to() *cobra.Command {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// From here until the agent is taken down, a signal
|
served, err := key.Serve(cmd.Context(), comment)
|
||||||
// cancels the context instead of ending the tool, so
|
|
||||||
// ssh ends and the socket and its directory are still
|
|
||||||
// removed.
|
|
||||||
ctx, stop := signals.Context(cmd.Context())
|
|
||||||
defer stop()
|
|
||||||
|
|
||||||
served, err := key.Serve(ctx, comment)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -61,7 +53,7 @@ func to() *cobra.Command {
|
|||||||
"-o", "IdentityAgent=" + served.Socket(),
|
"-o", "IdentityAgent=" + served.Socket(),
|
||||||
}, args)
|
}, args)
|
||||||
|
|
||||||
return connect(ctx, argv)
|
return connect(cmd.Context(), argv)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,7 +76,7 @@ func connect(ctx context.Context, argv []string) error {
|
|||||||
command.Stdout = os.Stdout
|
command.Stdout = os.Stdout
|
||||||
command.Stderr = os.Stderr
|
command.Stderr = os.Stderr
|
||||||
|
|
||||||
// A cancelled context means a signal arrived. Send ssh a
|
// A cancelled context means a signal ended the tool. Send ssh a
|
||||||
// SIGTERM rather than the default kill, so it puts the terminal
|
// SIGTERM rather than the default kill, so it puts the terminal
|
||||||
// back the way it found it before it goes.
|
// back the way it found it before it goes.
|
||||||
command.Cancel = func() error {
|
command.Cancel = func() error {
|
||||||
|
|||||||
+9
-153
@@ -20,13 +20,13 @@ import (
|
|||||||
|
|
||||||
// runAsTool, set in the environment of a re-executed test binary, tells
|
// runAsTool, set in the environment of a re-executed test binary, tells
|
||||||
// TestMain to run the tool through Main rather than the suite, so the
|
// TestMain to run the tool through Main rather than the suite, so the
|
||||||
// signal tests can drive the real signal path in a process they can
|
// signal test can drive the real signal path in a process it can send a
|
||||||
// send a signal to.
|
// signal to.
|
||||||
const runAsTool = "KEYFUNC_TEST_RUN_AS_TOOL"
|
const runAsTool = "KEYFUNC_TEST_RUN_AS_TOOL"
|
||||||
|
|
||||||
// TestMain re-executes the test binary as the tool when runAsTool is
|
// TestMain re-executes the test binary as the tool when runAsTool is
|
||||||
// set, and otherwise runs the suite. The signal tests start the tool
|
// set, and otherwise runs the suite. The signal test starts the tool
|
||||||
// this way, as a subprocess they can signal and watch end.
|
// this way, as a subprocess it can signal and watch clean up.
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
if os.Getenv(runAsTool) == "1" {
|
if os.Getenv(runAsTool) == "1" {
|
||||||
os.Exit(cli.Main())
|
os.Exit(cli.Main())
|
||||||
@@ -99,8 +99,6 @@ const marker = "KEYFUNC_TEST_MARKER"
|
|||||||
//
|
//
|
||||||
// The listing and the two ways a get can fail are worded as the
|
// The listing and the two ways a get can fail are worded as the
|
||||||
// OpenSSH client words them, each naming the path the server expanded.
|
// OpenSSH client words them, each naming the path the server expanded.
|
||||||
// A long listing (-n) writes each entry as the client does, its type
|
|
||||||
// first, so that a symlink shows as one.
|
|
||||||
// A listing fails one way when .ssh is not there and another when it is
|
// A listing fails one way when .ssh is not there and another when it is
|
||||||
// there but shut to the user; the first is the only failure read as a
|
// there but shut to the user; the first is the only failure read as a
|
||||||
// host with no file. A get fails one way for a file that is not there,
|
// host with no file. A get fails one way for a file that is not there,
|
||||||
@@ -139,7 +137,8 @@ while IFS= read -r line; do
|
|||||||
worked=yes
|
worked=yes
|
||||||
case "$1" in
|
case "$1" in
|
||||||
ls)
|
ls)
|
||||||
dir=$3
|
dir=$2
|
||||||
|
[ "$dir" = -1 ] && dir=$3
|
||||||
if [ ! -e "$home/$dir" ]; then
|
if [ ! -e "$home/$dir" ]; then
|
||||||
worked=no
|
worked=no
|
||||||
printf 'Can'\''t ls: "%s" not found\n' "$home/$dir" >&2
|
printf 'Can'\''t ls: "%s" not found\n' "$home/$dir" >&2
|
||||||
@@ -150,13 +149,7 @@ while IFS= read -r line; do
|
|||||||
else
|
else
|
||||||
for entry in "$home/$dir"/*; do
|
for entry in "$home/$dir"/*; do
|
||||||
[ -e "$entry" ] || continue
|
[ -e "$entry" ] || continue
|
||||||
name="$dir/$(basename "$entry")"
|
printf '%s/%s\n' "$dir" "$(basename "$entry")"
|
||||||
if [ "$2" = -n ]; then
|
|
||||||
printf '%s ? someone users 0 Oct 4 15:44 %s\n' \
|
|
||||||
"$(stat -c '%A' "$entry")" "$name"
|
|
||||||
else
|
|
||||||
printf '%s\n' "$name"
|
|
||||||
fi
|
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
@@ -216,18 +209,6 @@ fi
|
|||||||
sleep 5
|
sleep 5
|
||||||
`
|
`
|
||||||
|
|
||||||
// stalled is a stand-in for the system sftp that starts a child, notes
|
|
||||||
// it has started, and then blocks, so a test can signal the tool while
|
|
||||||
// sftp is running. The child holds the output the tool reads sftp
|
|
||||||
// through, as the ssh that sftp starts does, and is started before the
|
|
||||||
// note so that it is there when the signal ends the shell and still
|
|
||||||
// holds that output afterwards.
|
|
||||||
const stalled = `
|
|
||||||
sleep 5 &
|
|
||||||
touch "$KEYFUNC_TEST_STARTED"
|
|
||||||
wait
|
|
||||||
`
|
|
||||||
|
|
||||||
// pretended is where a stand-in writes down what it was asked to do.
|
// pretended is where a stand-in writes down what it was asked to do.
|
||||||
type pretended struct {
|
type pretended struct {
|
||||||
// home stands in for the home directory on the host.
|
// home stands in for the home directory on the host.
|
||||||
@@ -313,7 +294,7 @@ func TestTheFileIsUploadedBesideTheOldOneAndThenRenamedOverIt(t *testing.T) {
|
|||||||
|
|
||||||
// The listing fails on a host with no .ssh, so the get never runs;
|
// The listing fails on a host with no .ssh, so the get never runs;
|
||||||
// the write session then makes the directory and puts the file.
|
// the write session then makes the directory and puts the file.
|
||||||
require.Equal(t, "ls -n .ssh", sent[0])
|
require.Equal(t, "ls -1 .ssh", sent[0])
|
||||||
require.Equal(t, "-mkdir .ssh", sent[1])
|
require.Equal(t, "-mkdir .ssh", sent[1])
|
||||||
require.Equal(t, "chmod 700 .ssh", sent[2])
|
require.Equal(t, "chmod 700 .ssh", sent[2])
|
||||||
require.Equal(t, "put", strings.Fields(sent[3])[0])
|
require.Equal(t, "put", strings.Fields(sent[3])[0])
|
||||||
@@ -379,59 +360,6 @@ func TestADirectoryThatCannotBeEnteredIsRefusedBeforeAnyUpload(t *testing.T) {
|
|||||||
require.Equal(t, notADirectory, read(t, inTheWay))
|
require.Equal(t, notADirectory, read(t, inTheWay))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestASymlinkedFileIsRefusedBeforeAnyUpload(t *testing.T) {
|
|
||||||
t.Setenv(mnemonic.Variable, example())
|
|
||||||
|
|
||||||
pretend := pretendHost(t)
|
|
||||||
|
|
||||||
// The file the link points at, which the key would never reach.
|
|
||||||
target := filepath.Join(pretend.home, "keys")
|
|
||||||
require.NoError(t,
|
|
||||||
os.WriteFile(target, []byte("somebody else\n"), fileMode),
|
|
||||||
)
|
|
||||||
|
|
||||||
directory := filepath.Join(pretend.home, keptUnder)
|
|
||||||
require.NoError(t, os.Mkdir(directory, directoryMode))
|
|
||||||
|
|
||||||
link := filepath.Join(directory, keptIn)
|
|
||||||
require.NoError(t, os.Symlink(target, link))
|
|
||||||
|
|
||||||
printed, _, err := attempt(t, host)
|
|
||||||
require.ErrorIs(t, err, ssh.ErrSymlink)
|
|
||||||
require.Empty(t, printed)
|
|
||||||
|
|
||||||
// The read and nothing after it: no upload was tried, the link
|
|
||||||
// still points where it did, and what it points at is unchanged.
|
|
||||||
require.Equal(t, 1, connections(t, pretend))
|
|
||||||
|
|
||||||
pointsAt, err := os.Readlink(link)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, target, pointsAt)
|
|
||||||
require.Equal(t, "somebody else\n", read(t, target))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAnArgumentBesideTheHostIsRefusedBeforeAnyConnection(t *testing.T) {
|
|
||||||
t.Setenv(mnemonic.Variable, example())
|
|
||||||
|
|
||||||
pretend := pretendHost(t)
|
|
||||||
|
|
||||||
runs := [][]string{
|
|
||||||
{host, "frank@example.com"},
|
|
||||||
{host, "2222"},
|
|
||||||
{host, "frank@example.com", "--", "-P", "2222"},
|
|
||||||
{"--", host},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, args := range runs {
|
|
||||||
printed, _, err := attempt(t, args...)
|
|
||||||
require.ErrorIs(t, err, ssh.ErrStrayArgument)
|
|
||||||
require.Empty(t, printed)
|
|
||||||
}
|
|
||||||
|
|
||||||
// sftp was never started, so nothing was uploaded.
|
|
||||||
require.NoFileExists(t, pretend.arguments)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAnExistingDirectoryKeepsItsModeAndIsNotRemade(t *testing.T) {
|
func TestAnExistingDirectoryKeepsItsModeAndIsNotRemade(t *testing.T) {
|
||||||
t.Setenv(mnemonic.Variable, example())
|
t.Setenv(mnemonic.Variable, example())
|
||||||
|
|
||||||
@@ -557,22 +485,6 @@ func TestWhatComesAfterTheDashesIsGivenToSFTP(t *testing.T) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAProcessSFTPLeavesBehindDoesNotFailTheRun(t *testing.T) {
|
|
||||||
t.Setenv(mnemonic.Variable, example())
|
|
||||||
|
|
||||||
pretend := pretendHost(t)
|
|
||||||
|
|
||||||
// A session that works ends by leaving a child behind that holds
|
|
||||||
// sftp's output, as the master ssh leaves running for ControlPersist
|
|
||||||
// does under -v.
|
|
||||||
standIn(t, "sftp", installer+"sleep 5 &\n")
|
|
||||||
|
|
||||||
require.Equal(t, "added\n", install(t, host))
|
|
||||||
require.Equal(t, keyLine,
|
|
||||||
read(t, filepath.Join(pretend.home, keptUnder, keptIn)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSSHIsPointedAtTheAgentAndItsStatusIsHandedOn(t *testing.T) {
|
func TestSSHIsPointedAtTheAgentAndItsStatusIsHandedOn(t *testing.T) {
|
||||||
t.Setenv(mnemonic.Variable, example())
|
t.Setenv(mnemonic.Variable, example())
|
||||||
|
|
||||||
@@ -632,7 +544,7 @@ func TestASignalTakesTheAgentDirectoryDown(t *testing.T) {
|
|||||||
// ssh that blocks, waits until the agent is up and ssh is running
|
// ssh that blocks, waits until the agent is up and ssh is running
|
||||||
// against it, sends the tool the signal, and requires the agent socket
|
// against it, sends the tool the signal, and requires the agent socket
|
||||||
// and its directory to be gone once the tool has ended. The subprocess
|
// and its directory to be gone once the tool has ended. The subprocess
|
||||||
// goes through Main and the command's signal handling, so with that handling
|
// goes through Main and its signal handling, so with that handling
|
||||||
// removed the signal kills the tool outright, no deferred cleanup runs,
|
// removed the signal kills the tool outright, no deferred cleanup runs,
|
||||||
// the directory is left behind, and the check fails.
|
// the directory is left behind, and the check fails.
|
||||||
func signalEndsTheTool(t *testing.T, name string, signal os.Signal) {
|
func signalEndsTheTool(t *testing.T, name string, signal os.Signal) {
|
||||||
@@ -699,62 +611,6 @@ func waitForSocket(t *testing.T, noted string) string {
|
|||||||
return socket
|
return socket
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestASignalTakesTheInstallWorkingDirectoryDown(t *testing.T) {
|
|
||||||
t.Setenv(mnemonic.Variable, example())
|
|
||||||
|
|
||||||
for _, ending := range []os.Signal{
|
|
||||||
syscall.SIGTERM, syscall.SIGINT, syscall.SIGHUP,
|
|
||||||
} {
|
|
||||||
signalEndsTheInstall(t, ending.String(), ending)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// signalEndsTheInstall runs "ssh install" as a subprocess against a
|
|
||||||
// stand-in sftp that blocks, with a temporary directory of the test's
|
|
||||||
// own, waits until sftp is running, sends the tool the signal, and
|
|
||||||
// requires the tool to end within a second with status 1 and the
|
|
||||||
// working directory it made there to be gone.
|
|
||||||
func signalEndsTheInstall(t *testing.T, name string, signal os.Signal) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
temporary := t.TempDir()
|
|
||||||
started := filepath.Join(t.TempDir(), "started")
|
|
||||||
t.Setenv("KEYFUNC_TEST_STARTED", started)
|
|
||||||
standIn(t, "sftp", stalled)
|
|
||||||
|
|
||||||
//nolint:gosec // the binary is this test's own, re-run as the tool
|
|
||||||
command := exec.CommandContext(
|
|
||||||
t.Context(), os.Args[0], subcommand, installing, host,
|
|
||||||
)
|
|
||||||
|
|
||||||
command.Env = append(os.Environ(), runAsTool+"=1", "TMPDIR="+temporary)
|
|
||||||
require.NoError(t, command.Start())
|
|
||||||
|
|
||||||
// sftp is started only once the working directory has been made.
|
|
||||||
require.Eventually(t, func() bool {
|
|
||||||
_, err := os.Stat(started)
|
|
||||||
|
|
||||||
return err == nil
|
|
||||||
}, 5*time.Second, 5*time.Millisecond)
|
|
||||||
|
|
||||||
working, err := os.ReadDir(temporary)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Len(t, working, 1, name)
|
|
||||||
|
|
||||||
sent := time.Now()
|
|
||||||
|
|
||||||
require.NoError(t, command.Process.Signal(signal))
|
|
||||||
waitForTool(t, name, command)
|
|
||||||
|
|
||||||
// The child sftp started would hold sftp's output for seconds yet.
|
|
||||||
require.Less(t, time.Since(sent), time.Second, name)
|
|
||||||
require.Equal(t, failedStatus, command.ProcessState.ExitCode(), name)
|
|
||||||
|
|
||||||
left, err := os.ReadDir(temporary)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Empty(t, left, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTheMnemonicIsNotHandedToSFTP(t *testing.T) {
|
func TestTheMnemonicIsNotHandedToSFTP(t *testing.T) {
|
||||||
t.Setenv(mnemonic.CommandVariable, "echo "+example())
|
t.Setenv(mnemonic.CommandVariable, "echo "+example())
|
||||||
t.Setenv(mnemonic.Variable, example())
|
t.Setenv(mnemonic.Variable, example())
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"git.eeqj.de/sneak/secret/pkg/bip85"
|
||||||
"github.com/btcsuite/btcd/btcutil/hdkeychain"
|
"github.com/btcsuite/btcd/btcutil/hdkeychain"
|
||||||
"github.com/btcsuite/btcd/chaincfg"
|
"github.com/btcsuite/btcd/chaincfg"
|
||||||
"sneak.berlin/go/keyfunc/internal/bip39"
|
bip39 "github.com/tyler-smith/go-bip39"
|
||||||
"sneak.berlin/go/secret/pkg/bip85"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
bip39 "github.com/tyler-smith/go-bip39"
|
||||||
"golang.org/x/term"
|
"golang.org/x/term"
|
||||||
"sneak.berlin/go/keyfunc/internal/bip39"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
{
|
{
|
||||||
"license": "MIT",
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"prettier": "3.8.1"
|
"prettier": "3.8.1"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user