Compare commits
1
Commits
main
..
983f554ac0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
983f554ac0 |
+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
|
||||||
|
|||||||
@@ -69,8 +69,6 @@ calls into `internal/`. The packages there are:
|
|||||||
`cli/options` holds the flags every command shares, `cli/signals` catches
|
`cli/options` holds the flags every command shares, `cli/signals` catches
|
||||||
SIGINT, SIGTERM and SIGHUP for the commands that clean up before they end, and
|
SIGINT, SIGTERM and SIGHUP for the commands that clean up before they end, and
|
||||||
`cli/ssh`, `cli/age` and `cli/mnemonic` are the command groups.
|
`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 +177,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,15 +250,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
|
A regular file already at the `-o` path is replaced, and the new file has mode
|
||||||
error, under any name such as `/dev/stdout` or `/dev/fd/2`, is written to that
|
`0600`. A symlink there is followed, and what it points at is treated the same
|
||||||
stream, as leaving out `-o` writes to standard output; the file the stream is
|
way, so the link keeps pointing where it did; a symlink that points at nothing
|
||||||
redirected to is written as the redirect says and never replaced, so with `>>`
|
is refused. A named pipe or a device, such as `/dev/null` or `/dev/stdout`, is
|
||||||
the output follows what the file already held. Otherwise, a regular file already
|
written to directly.
|
||||||
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>]`
|
||||||
|
|
||||||
@@ -312,12 +303,11 @@ 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
|
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
|
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
|
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
|
named pipe or a device at the `-o` path is written to directly, and the signal
|
||||||
standard output or standard error, is written to directly, and the signal ends
|
ends the tool there as it ends any other command. While `ssh to` or
|
||||||
the tool there as it ends any other command. While `ssh to` or `ssh install` has
|
`ssh install` has `ssh` or `sftp` running, the signal ends that program instead,
|
||||||
`ssh` or `sftp` running, the signal ends that program instead, the tool removes
|
the tool removes its agent socket or working files, and it exits with status 1,
|
||||||
its agent socket or working files, and it exits with status 1, or for `ssh to`
|
or for `ssh to` with `ssh`'s own status if `ssh` reported one.
|
||||||
with `ssh`'s own status if `ssh` reported one.
|
|
||||||
|
|
||||||
## Entrypoints
|
## Entrypoints
|
||||||
|
|
||||||
@@ -375,7 +365,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).
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ require (
|
|||||||
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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -35,10 +35,16 @@ github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3C
|
|||||||
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=
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
"git.eeqj.de/sneak/secret/pkg/bip85"
|
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
)
|
)
|
||||||
|
|||||||
+4
-32
@@ -141,10 +141,7 @@ func runDecrypt(cmd *cobra.Command, args []string) error {
|
|||||||
|
|
||||||
// through opens the input the arguments ask for and hands it to the
|
// through opens the input the arguments ask for and hands it to the
|
||||||
// work, with the file --output names to write to, or the command's own
|
// work, with the file --output names to write to, or the command's own
|
||||||
// output when it names none or names the same file as that output, and
|
// output when it names none.
|
||||||
// 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,
|
||||||
@@ -161,36 +158,11 @@ func through(
|
|||||||
return fmt.Errorf("reading the output file: %w", err)
|
return fmt.Errorf("reading the output file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch {
|
if name == "" {
|
||||||
case name == "", same(name, cmd.OutOrStdout()):
|
|
||||||
return work(cmd.OutOrStdout(), src)
|
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
|
|
||||||
// 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()
|
return output(name, src, work)
|
||||||
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
|
||||||
@@ -232,7 +204,7 @@ func output(
|
|||||||
case info.Mode().IsRegular():
|
case info.Mode().IsRegular():
|
||||||
return replace(name, src, work)
|
return replace(name, src, work)
|
||||||
case info.Mode().Type() == fs.ModeSymlink:
|
case info.Mode().Type() == fs.ModeSymlink:
|
||||||
// os.Stat follows the link as opening it would. /dev/fd/3
|
// os.Stat follows the link as opening it would. /dev/stdout
|
||||||
// needs that: it reaches a pipe or a terminal through a link
|
// needs that: it reaches a pipe or a terminal through a link
|
||||||
// that names no path.
|
// that names no path.
|
||||||
info, err = os.Stat(name)
|
info, err = os.Stat(name)
|
||||||
|
|||||||
@@ -151,58 +151,6 @@ func TestANamedPipeAtTheOutputPathIsWrittenToAndStaysAPipe(t *testing.T) {
|
|||||||
require.Equal(t, "the secret\n", run(t, "age", "decrypt", sealedFile))
|
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) {
|
func TestASignalStopsAnEncryptionAndLeavesNoFile(t *testing.T) {
|
||||||
t.Setenv(mnemonic.Variable, example())
|
t.Setenv(mnemonic.Variable, example())
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -49,20 +49,6 @@ 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 +60,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 {
|
||||||
@@ -285,23 +256,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 +326,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"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
;;
|
;;
|
||||||
@@ -313,7 +306,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 +372,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())
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"git.eeqj.de/sneak/secret/pkg/bip85"
|
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
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