Compare commits

...
10 Commits
Author SHA1 Message Date
sneak f53b6aad42 Add sutil CLI in cmd/sutil with internal/cli command tree 2026-10-31 10:03:06 -07:00
sneak c9ce5ff442 Add internal randstream and pwgen packages 2026-10-31 10:03:06 -07:00
sneak b7028fd7e3 Implement pkg/cbk: crypto_box keypairs from BIP39 mnemonics 2026-10-31 10:03:06 -07:00
sneak 02386a6ff1 Rename module to sneak.berlin/go/x and rework bip39wordlist
Move to Go 1.26, embed the wordlist from english.txt, add
ValidateWordlist, NormalizeWordlist, and GenerateMnemonic with tests,
and remove the old root-level CLI that used the previous module path.
2026-10-31 10:03:06 -07:00
sneak 4d27e30ea8 Add hash-pinned Dockerfile and Gitea workflow, drop Drone 2026-10-31 10:03:06 -07:00
sneak 157e5ef124 Add Makefile with test, lint, fmt, check, docker, and hooks targets 2026-10-31 10:03:06 -07:00
sneak 7be0fa7060 Add canonical .golangci.yml 2026-10-31 10:03:06 -07:00
sneak 386155a480 Add editor, ignore, and prettier configuration 2026-10-31 10:03:06 -07:00
sneak 3510f8a144 Add WTFPL LICENSE and REPO_POLICIES.md 2026-10-31 10:03:06 -07:00
sneak 6f9dcc8f93 Rewrite README with required sections and package docs 2026-10-31 10:03:06 -07:00
43 changed files with 4900 additions and 2548 deletions

No files matched your search

+9
View File
@@ -0,0 +1,9 @@
.git
node_modules
.DS_Store
/sutil
*.test
*.out
*.log
*.prof
coverage.*
-10
View File
@@ -1,10 +0,0 @@
kind: pipeline
name: test-go
steps:
- name: test-go
image: golang
commands:
- go test ./...
#- go get github.com/golangci/golangci-lint/cmd/golangci-lint@v1.31.0
#- golangci-lint run
+12
View File
@@ -0,0 +1,12 @@
root = true
[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[Makefile]
indent_style = tab
+9
View File
@@ -0,0 +1,9 @@
name: check
on: [push]
jobs:
check:
runs-on: ubuntu-latest
steps:
# actions/checkout v4.2.2, 2026-02-22
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- run: docker build .
+29 -1
View File
@@ -1 +1,29 @@
/x
# OS
.DS_Store
Thumbs.db
# Editors
*.swp
*.swo
*~
*.bak
.idea/
.vscode/
*.sublime-*
# Node (prettier for markdown formatting)
node_modules/
# Environment / secrets
.env
.env.*
*.pem
*.key
# Go build and test artifacts
/sutil
*.test
*.out
*.log
*.prof
coverage.*
+32
View File
@@ -0,0 +1,32 @@
version: "2"
run:
timeout: 5m
modules-download-mode: readonly
linters:
default: all
disable:
# Genuinely incompatible with project patterns
- exhaustruct # Requires all struct fields
- depguard # Dependency allow/block lists
- godot # Requires comments to end with periods
- wsl # Deprecated, replaced by wsl_v5
- wrapcheck # Too verbose for internal packages
- varnamelen # Short names like db, id are idiomatic Go
linters-settings:
lll:
line-length: 88
funlen:
lines: 80
statements: 50
cyclop:
max-complexity: 15
dupl:
threshold: 100
issues:
exclude-use-default: false
max-issues-per-linter: 0
max-same-issues: 0
+2
View File
@@ -0,0 +1,2 @@
node_modules/
yarn.lock
+4
View File
@@ -0,0 +1,4 @@
{
"tabWidth": 4,
"proseWrap": "always"
}
+60
View File
@@ -0,0 +1,60 @@
# The checks behind `make check` are split across independent stages so that
# formatting and lint failures surface in seconds, before any compilation:
#
# lint - gofmt and golangci-lint (make fmt-check-go, make lint)
# docs - prettier check of all markdown (make fmt-check-md)
# builder - tests (make test) and the release build
#
# The builder stage declares explicit dependencies on lint and docs so
# BuildKit cannot finish the build unless every check passed.
# Lint stage: fast feedback on Go formatting and lint issues
# golangci/golangci-lint:v2.12.2, 2026-05-06
# Pinned below v2.13.0 until the canonical .golangci.yml disables
# exhaustruct_v5 (see the TODO in README.md). Keep in sync with Makefile.
FROM golangci/golangci-lint@sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240 AS lint
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN make fmt-check-go
RUN make lint
# Docs stage: markdown formatting check with the pinned prettier
# node:24-alpine, 2026-10-28
FROM node@sha256:8a32c8cf649a49915a66e81e7a609223c708d0adabf288ee3f4077d765542f85 AS docs
RUN apk add --no-cache make
WORKDIR /src
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile
COPY . .
RUN make fmt-check-md
# Build stage
# golang:1.26-alpine, 2026-10-27
FROM golang@sha256:a2012b4a1d1f9f7e9f44e0d39b805a901e856b1dd80b4e6ee4338db547050308 AS builder
# gcc and musl-dev are needed only because `make test` uses -race, which
# requires cgo; the release binary below is built with CGO_ENABLED=0.
RUN apk add --no-cache make gcc musl-dev
WORKDIR /src
# Force BuildKit to run the lint and docs stages before proceeding
COPY --from=lint /src/go.sum /dev/null
COPY --from=docs /src/package.json /dev/null
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN make test
ARG VERSION=dev
RUN CGO_ENABLED=0 go build -trimpath \
-ldflags="-s -w -X main.version=${VERSION}" \
-o /sutil ./cmd/sutil
# Runtime stage: the binary plus the full source it was built from
# alpine:3.24, 2026-10-23
FROM alpine@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a
COPY --from=builder /sutil /usr/local/bin/sutil
COPY --from=builder /src /usr/local/src/x
ENTRYPOINT ["sutil"]
+14
View File
@@ -0,0 +1,14 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
Copyright (C) 2020-2026 sneak <sneak@sneak.berlin>
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.
+108
View File
@@ -0,0 +1,108 @@
# sneak.berlin/go/x
#
# This Makefile is the single source of truth for how this repo is built,
# tested, linted, and formatted. Always use these targets rather than
# invoking the underlying tools directly.
.PHONY: all build install test lint fmt fmt-go fmt-md fmt-check \
fmt-check-go fmt-check-md check docker hooks tidy setup clean
BINARY := sutil
VERSION := $(shell git describe --always --dirty 2>/dev/null || echo dev)
GOLDFLAGS := -X main.version=$(VERSION)
GO_DIRS := cmd internal pkg
# golangci/golangci-lint:v2.12.2, 2026-05-06
# Pinned below v2.13.0 until the canonical .golangci.yml disables
# exhaustruct_v5 (see the TODO in README.md). Keep in sync with Dockerfile.
GOLANGCI_LINT_IMAGE := golangci/golangci-lint@sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240
# flags are repeated here (also in .prettierrc) so this Makefile works
# standalone when copied as a template
PRETTIER := yarn run --silent prettier
PRETTIER_FLAGS := --tab-width 4 --prose-wrap always
MARKDOWN_GLOB := '**/*.md'
all: build
# Build the sutil binary in the repo root with the git version embedded.
build:
go build -trimpath -ldflags "$(GOLDFLAGS)" -o $(BINARY) ./cmd/$(BINARY)
# Install sutil into $(go env GOBIN) (or $GOPATH/bin).
install:
go install -trimpath -ldflags "$(GOLDFLAGS)" ./cmd/$(BINARY)
# Run the tests quietly; on failure rerun verbosely for diagnostics and
# still fail.
test:
@go test -timeout 30s -race -cover ./... || \
{ echo "--- Rerunning with -v for details ---"; \
go test -timeout 30s -race -v ./...; exit 1; }
# Lint with the pinned golangci-lint image when docker is available so local
# results match CI exactly. Inside the Docker build (where there is no
# docker) the image's own golangci-lint binary is used. The host module
# cache is shared read-only; the linter and build caches live in named
# volumes so repeated runs are fast.
lint:
@if command -v docker >/dev/null 2>&1; then \
docker run --rm \
-v "$(CURDIR)":/src \
-v "$$(go env GOMODCACHE)":/go/pkg/mod:ro \
-v sutil-golangci-lint-cache:/root/.cache/golangci-lint \
-v sutil-go-build-cache:/root/.cache/go-build \
-w /src $(GOLANGCI_LINT_IMAGE) golangci-lint run ./...; \
else \
golangci-lint run ./...; \
fi
# Format everything (writes files).
fmt: fmt-go fmt-md
fmt-go:
gofmt -w $(GO_DIRS)
fmt-md:
$(PRETTIER) --write $(MARKDOWN_GLOB) $(PRETTIER_FLAGS)
# Check formatting without writing anything.
fmt-check: fmt-check-go fmt-check-md
fmt-check-go:
@unformatted="$$(gofmt -l $(GO_DIRS))"; \
if [ -n "$$unformatted" ]; then \
echo "gofmt: the following files need formatting:"; \
echo "$$unformatted"; \
exit 1; \
fi
fmt-check-md:
$(PRETTIER) --check $(MARKDOWN_GLOB) $(PRETTIER_FLAGS)
# Everything CI gates on. Must not modify any files.
check: test lint fmt-check
# Build the Docker image; the Dockerfile runs the same checks as `make check`.
docker:
docker build -t $(BINARY) .
# Install the pre-commit hook.
hooks:
@printf '#!/bin/sh\nset -e\n' > .git/hooks/pre-commit
@if [ -f go.mod ]; then \
printf 'go mod tidy\ngo fmt ./...\ngit diff --exit-code -- go.mod go.sum || { echo "go mod tidy changed files; please stage and retry"; exit 1; }\n' >> .git/hooks/pre-commit; \
fi
@printf 'make check\n' >> .git/hooks/pre-commit
@chmod +x .git/hooks/pre-commit
# Tidy go.mod and go.sum.
tidy:
go mod tidy
# One-time developer setup: install the pinned prettier for markdown checks.
setup:
yarn install --frozen-lockfile
clean:
rm -f $(BINARY)
+267 -7
View File
@@ -1,16 +1,276 @@
# x
Experimental code.
## Description
# Build Status
`sneak.berlin/go/x` is a WTFPL-licensed Go module and CLI tool by
[@sneak](https://sneak.berlin) that serves as an umbrella publishing space for
small reusable packages, plus `sutil`, a command-line utility that exposes them
from the shell.
[![Build Status](https://drone.datavi.be/api/badges/sneak/x/status.svg)](https://drone.datavi.be/sneak/x)
Importable packages live in `pkg/`. The CLI is `cmd/sutil/`, and the code that
exists only to support it lives in `internal/`.
# License
## Getting Started
WTFPL
### Install the CLI
# Author
```sh
go install sneak.berlin/go/x/cmd/sutil@latest
```
sneak@sneak.berlin
Or, from a checkout:
```sh
make build # builds ./sutil with the git version embedded
make install # installs sutil into $(go env GOBIN)
```
### Use the CLI
```sh
# An endless stream of fast pseudorandom bytes; stop it with Ctrl-C.
sutil generate random | pv > /dev/null
# The same, but deterministic: the same seed always produces the same bytes.
sutil generate random --count 1073741824 "my seed" > testdata.bin
# A memorable password: three dictionary words, each followed by a digit,
# joined by a randomly chosen delimiter. Requires a system word list.
sutil generate password
# marble7-sprout2-velvet9
# A new 24-word BIP39 mnemonic phrase (or --words 12).
sutil generate mnemonic
# A crypto_box keypair derived from a mnemonic. The mnemonic is read from
# stdin, never from the command line, so it stays out of shell history.
sutil generate mnemonic > mnemonic.txt
sutil cbk derive < mnemonic.txt
sutil cbk derive --public < mnemonic.txt
# Build metadata.
sutil version
sutil info
```
### Use the packages
```go
package main
import (
"fmt"
"sneak.berlin/go/x/pkg/bip39wordlist"
"sneak.berlin/go/x/pkg/cbk"
)
func main() {
mnemonic, err := bip39wordlist.GenerateMnemonic(bip39wordlist.MaxWordCount)
if err != nil {
panic(err)
}
// The same mnemonic (and optional passphrase) always yields the same
// keypair, on any machine.
alice, err := cbk.New(cbk.Params{Mnemonic: mnemonic})
if err != nil {
panic(err)
}
bob, err := cbk.FromMnemonic("abandon abandon abandon abandon abandon " +
"abandon abandon abandon abandon abandon abandon about")
if err != nil {
panic(err)
}
sealed, err := alice.Seal([]byte("hello bob"), bob.PublicKey)
if err != nil {
panic(err)
}
plain, err := bob.Open(sealed, alice.PublicKey)
if err != nil {
panic(err)
}
fmt.Println(string(plain), alice.PublicKeyHex())
}
```
## Rationale
- **Reusable odds and ends need a home.** Several small packages are worth
sharing between projects but are too small to each deserve a repository, CI
pipeline, and release cadence of their own. This module collects them under
one import path.
- **`/dev/urandom` is slow.** Operating system random devices are tuned for
small requests of key material, not for filling a disk or producing gigabytes
of test data. `sutil generate random` streams pseudorandom bytes at close to
memory bandwidth, and can do so deterministically from a seed so test data is
reproducible.
- **Keys you can carry in your head.** A crypto_box keypair derived from a BIP39
mnemonic can be regenerated anywhere from a phrase a human can write down or
memorize, with no key file to lose. `pkg/cbk` makes that derivation explicit
and reproducible.
- **Passwords you can type.** Three dictionary words with digits and a delimiter
are far easier to transcribe than line noise, and with the macOS word list
they still carry about 62 bits of entropy.
## Design
### Layout
| Path | Purpose |
| ---------------------- | --------------------------------------------------- |
| `cmd/sutil/` | `package main`; the whole entrypoint is a few lines |
| `internal/cli/` | The cobra command tree, built by `cli.New` |
| `internal/randstream/` | Fast seeded pseudorandom byte stream |
| `internal/pwgen/` | Password generator over the system dictionary |
| `pkg/bip39wordlist/` | BIP39 English wordlist and mnemonic helpers |
| `pkg/cbk/` | crypto_box keypairs derived from BIP39 mnemonics |
The split between `pkg/` and `internal/` is deliberate and should be kept:
`pkg/` holds only packages designed to be imported by other programs, with a
stable, documented API. Anything that exists to support `sutil` goes under
`internal/`, even if it would be harmless to export, so that the public surface
of the module stays small and intentional.
The command tree holds no global state: `cli.New` builds every command from a
`Params` struct, so tests construct it repeatedly with captured input and
output, and nothing happens in `init` functions.
### `pkg/cbk`
Derives NaCl crypto_box keypairs (Curve25519, XSalsa20, Poly1305)
deterministically from BIP39 mnemonics, and seals and opens messages with them.
The derivation is fixed and documented so keys are reproducible:
1. The mnemonic is validated and canonicalized with
`bip39wordlist.NormalizeWordlist`.
2. The standard 64-byte BIP39 seed is computed with PBKDF2-HMAC-SHA512 (2048
rounds, salt `"mnemonic" + passphrase`), exactly as a BIP39 wallet would, so
the optional passphrase works as the usual "25th word".
3. The 32-byte private key is
`HKDF-SHA256(seed, salt = nil, info = cbk.DerivationInfo)`.
4. The public key is the Curve25519 base-point multiple of the private key.
A sealed message is a 24-byte random nonce followed by the box ciphertext, so it
can be handled as a single opaque byte string. The API:
- `cbk.New(cbk.Params{Mnemonic, Passphrase})` and `cbk.FromMnemonic(m)` return a
`CryptoBoxKeypair`.
- `CryptoBoxKeypair.Seal(message, recipientPublicKey)` and
`CryptoBoxKeypair.Open(sealed, senderPublicKey)`.
- `CryptoBoxKey.Hex()`, `CryptoBoxKey.Bytes()`, `cbk.KeyFromHex(s)`, and the
`PublicKeyHex`, `PrivateKeyHex`, `PublicKeyBytes`, `PrivateKeyBytes` accessors
on the keypair.
The test suite pins the keypair derived from the standard all-zero-entropy BIP39
test vector. If that test ever fails, the derivation has changed and existing
keys are no longer reproducible.
### `pkg/bip39wordlist`
The BIP39 English wordlist, embedded from `english.txt` and checked against the
reference implementation in the tests, plus mnemonic helpers:
- `ValidateWordlist(mnemonic)` checks word count, every word, and the checksum,
ignoring letter case and whitespace. It reports what is wrong with sentinel
errors (`ErrInvalidWordCount`, `ErrUnknownWord` naming the word and its
position, `ErrInvalidChecksum`).
- `NormalizeWordlist(mnemonic)` returns the canonical lowercase,
single-space-separated form, and refuses anything invalid.
- `GenerateMnemonic(wordCount)` makes a new mnemonic of 12, 15, 18, 21, or 24
words from `crypto/rand`.
- `Contains(word)`, `ValidWordCount(n)`, and the `WordlistEnglish` and
`Wordlist` tables.
### `internal/randstream`
An `io.Reader` that expands a 32-byte key with the ChaCha8 generator from
`math/rand/v2`. With a seed string the key is its SHA-256 and the stream is
deterministic; without one the key comes from `crypto/rand`, which is the only
time the operating system's random source is touched. The output is for bulk
data, not for cryptographic keys.
### `internal/pwgen`
Produces passwords of the form
```
<word><digit><delim><word><digit><delim><word><digit>
```
where the delimiter is drawn once per password from `.-_|~+=:,!@#%^&*` and used
in both positions. Words come from the system word list
(`/usr/share/dict/words`) through `github.com/kavfixnel/words`, filtered to
lowercase ASCII words of four to eight letters, which drops proper nouns and
abbreviations. All choices use `crypto/rand`. On a system without a word list,
`sutil generate password` panics rather than falling back to something weaker;
the upstream library is only known to work on macOS.
### CLI
```
sutil
├── version print the build version
├── info print version, Go version, module, VCS metadata
├── generate (gen)
│ ├── random [seedstr] stream pseudorandom bytes (--count N to stop)
│ ├── password (pw) print a memorable password
│ └── mnemonic print a new BIP39 mnemonic (--words N)
└── cbk
└── derive keypair from a mnemonic on stdin (--public)
```
### Build, test, and CI
The `Makefile` is the single entry point: `make test` (30-second timeout, race
detector, quiet unless something fails), `make lint`, `make fmt` and
`make fmt-check` (gofmt for Go, prettier for markdown), `make check` for all
three, `make build`, `make install`, `make docker`, `make hooks`, `make tidy`,
and `make setup` to install the pinned prettier.
`make lint` runs golangci-lint inside the same hash-pinned Docker image the CI
build uses, so local results match CI exactly. The `Dockerfile` splits
`make check` across stages: a lint stage and a markdown-check stage fail fast in
seconds, and the build stage, which runs the tests and compiles `sutil`, depends
on both. The runtime image carries the binary and the source it was built from.
Every external reference, from base images to the GitHub Action in
`.gitea/workflows/check.yml`, is pinned by content hash with a version and date
comment. `.golangci.yml` is the standard configuration and is never edited by
hand here.
## Development
```sh
make setup # once: install the pinned prettier with yarn
make check # test + lint + fmt-check, exactly what CI gates on
make fmt # gofmt and prettier, writes files
make hooks # install the pre-commit hook that runs make check
make docker # the full CI build
```
## TODO
- [ ] Tag `v0.1.0` once `main` is green in CI.
- [ ] Bump the pinned `golangci/golangci-lint` image (in `Makefile` and
`Dockerfile`) past v2.12.2 once the canonical `.golangci.yml` disables
`exhaustruct_v5`, which v2.13.0 added alongside the already-disabled
`exhaustruct`, and replaces the deprecated `gomodguard` with
`gomodguard_v2`. With `default: all`, newer images enable `exhaustruct_v5`
and fail every partial struct literal.
- [ ] Add `sutil cbk seal` and `sutil cbk open` so messages can be sealed and
opened from the shell, not just from Go.
- [ ] Let `sutil cbk derive` take the BIP39 passphrase from an environment
variable; the library supports it, the CLI does not expose it yet.
## License
WTFPL. See `LICENSE`.
## Author
[@sneak](https://sneak.berlin)
&lt;[sneak@sneak.berlin](mailto:sneak@sneak.berlin)&gt;
+368
View File
@@ -0,0 +1,368 @@
---
title: Repository Policies
last_modified: 2026-07-06
---
This document covers repository structure, tooling, and workflow standards. Code
style conventions are in separate documents:
- [Code Styleguide](https://git.eeqj.de/sneak/prompts/raw/branch/main/prompts/CODE_STYLEGUIDE.md)
(general, bash, Docker)
- [Go](https://git.eeqj.de/sneak/prompts/raw/branch/main/prompts/CODE_STYLEGUIDE_GO.md)
- [JavaScript](https://git.eeqj.de/sneak/prompts/raw/branch/main/prompts/CODE_STYLEGUIDE_JS.md)
- [Python](https://git.eeqj.de/sneak/prompts/raw/branch/main/prompts/CODE_STYLEGUIDE_PYTHON.md)
- [Go HTTP Server Conventions](https://git.eeqj.de/sneak/prompts/raw/branch/main/prompts/GO_HTTP_SERVER_CONVENTIONS.md)
---
- Cross-project documentation (such as this file) must include
`last_modified: YYYY-MM-DD` in the YAML front matter so it can be kept in sync
with the authoritative source as policies evolve.
- **ALL external references must be pinned by cryptographic hash.** This
includes Docker base images, Go modules, npm packages, GitHub Actions, and
anything else fetched from a remote source. Version tags (`@v4`, `@latest`,
`:3.21`, etc.) are server-mutable and therefore remote code execution
vulnerabilities. The ONLY acceptable way to reference an external dependency
is by its content hash (Docker `@sha256:...`, Go module hash in `go.sum`, npm
integrity hash in lockfile, GitHub Actions `@<commit-sha>`). No exceptions.
This also means never `curl | bash` to install tools like pyenv, nvm, rustup,
etc. Instead, download a specific release archive from GitHub, verify its hash
(hardcoded in the Dockerfile or script), and only then install. Unverified
install scripts are arbitrary remote code execution. This is the single most
important rule in this document. Double-check every external reference in
every file before committing. There are zero exceptions to this rule.
- Every repo with software must have a root `Makefile` with these targets:
`make test`, `make lint`, `make fmt` (writes), `make fmt-check` (read-only),
`make check` (prereqs: `test`, `lint`, `fmt-check`), `make docker`, and
`make hooks` (installs pre-commit hook). A model Makefile is at
`https://git.eeqj.de/sneak/prompts/raw/branch/main/Makefile`.
- Always use Makefile targets (`make fmt`, `make test`, `make lint`, etc.)
instead of invoking the underlying tools directly. The Makefile is the single
source of truth for how these operations are run.
- The Makefile is authoritative documentation for how the repo is used. Beyond
the required targets above, it should have targets for every common operation:
running a local development server (`make run`, `make dev`), re-initializing
or migrating the database (`make db-reset`, `make migrate`), building
artifacts (`make build`), generating code, seeding data, or anything else a
developer would do regularly. If someone checks out the repo and types
`make<tab>`, they should see every meaningful operation available. A new
contributor should be able to understand the entire development workflow by
reading the Makefile.
- Every repo should have a `Dockerfile`. All Dockerfiles must run `make check`
as a build step so the build fails if the branch is not green. For non-server
repos, the Dockerfile should bring up a development environment and run
`make check`. For server repos, `make check` should run as an early build
stage before the final image is assembled.
- **Dockerfiles must use a separate lint stage for fail-fast feedback.** Go
repos use a multistage build where linting runs in an independent stage based
on the `golangci/golangci-lint` image (pinned by hash). This stage runs
`make fmt-check` and `make lint` before the full build begins. The build stage
then declares an explicit dependency on the lint stage via
`COPY --from=lint /src/go.sum /dev/null`, which forces BuildKit to complete
linting before proceeding to compilation and tests. This ensures lint failures
surface in seconds rather than minutes, without blocking on dependency
download or compilation in the build stage.
The standard pattern for a Go repo Dockerfile is:
```dockerfile
# Lint stage — fast feedback on formatting and lint issues
# golangci/golangci-lint:v2.x.x, YYYY-MM-DD
FROM golangci/golangci-lint@sha256:... AS lint
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN make fmt-check
RUN make lint
# Build stage
# golang:1.x-alpine, YYYY-MM-DD
FROM golang@sha256:... AS builder
WORKDIR /src
# Force BuildKit to run the lint stage before proceeding
COPY --from=lint /src/go.sum /dev/null
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN make test
ARG VERSION=dev
RUN CGO_ENABLED=0 go build -trimpath \
-ldflags="-s -w -X main.Version=${VERSION}" \
-o /app ./cmd/app/
# Runtime stage
FROM alpine@sha256:...
COPY --from=builder /app /usr/local/bin/app
ENTRYPOINT ["app"]
```
Key points:
- The lint stage uses the `golangci/golangci-lint` image directly (it
includes both Go and the linter), so there is no need to install the
linter separately.
- `COPY --from=lint /src/go.sum /dev/null` is a no-op file copy that creates
a stage dependency. BuildKit runs stages in parallel by default; without
this line, the build stage would not wait for lint to finish and a lint
failure might not fail the overall build.
- If the project uses `//go:embed` directives that reference build artifacts
(e.g. a web frontend compiled in a separate stage), the lint stage must
create placeholder files so the embed directives resolve. Example:
`RUN mkdir -p web/dist && touch web/dist/index.html web/dist/style.css`.
The lint stage should not depend on the actual build output — it exists to
fail fast.
- If the project requires CGO or system libraries for linting (e.g.
`vips-dev`), install them in the lint stage with `apk add`.
- The build stage runs `make test` after compilation setup. Tests run in the
build stage, not the lint stage, because they may require compiled
artifacts or heavier dependencies.
- Every repo should have a Gitea Actions workflow (`.gitea/workflows/`) that
runs `docker build .` on push. Since the Dockerfile already runs `make check`,
a successful build implies all checks pass.
- Use platform-standard formatters: `black` for Python, `prettier` for
JS/CSS/Markdown/HTML, `go fmt` for Go. Always use default configuration with
two exceptions: four-space indents (except Go), and `proseWrap: always` for
Markdown (hard-wrap at 80 columns). Documentation and writing repos (Markdown,
HTML, CSS) should also have `.prettierrc` and `.prettierignore`.
- Pre-commit hook: `make check` if local testing is possible, otherwise
`make lint && make fmt-check`. The Makefile should provide a `make hooks`
target to install the pre-commit hook.
- All repos with software must have tests that run via the platform-standard
test framework (`go test`, `pytest`, `jest`/`vitest`, etc.). If no meaningful
tests exist yet, add the most minimal test possible — e.g. importing the
module under test to verify it compiles/parses. There is no excuse for
`make test` to be a no-op.
- `make test` must complete in under 20 seconds. Add a 30-second timeout in the
Makefile.
- **`make test` should use the conditional verbose rerun pattern.** Run tests
without `-v` (verbose) first. If tests fail, automatically rerun with `-v` to
show full output. This keeps CI logs and `docker build` output clean on
success (just package/suite summaries) while providing full diagnostic detail
on failure (every test case, every assertion). The general shell pattern:
```makefile
test:
@<test-command> || \
{ echo "--- Rerunning with -v for details ---"; \
<test-command-with-v>; exit 1; }
```
Go example:
```makefile
test:
@go test -timeout 30s -race -cover ./... || \
{ echo "--- Rerunning with -v for details ---"; \
go test -timeout 30s -race -v ./...; exit 1; }
```
Python example:
```makefile
test:
@python -m pytest || \
{ echo "--- Rerunning with -v for details ---"; \
python -m pytest -v; exit 1; }
```
The `exit 1` ensures the target always fails after a rerun — the first run
already proved the tests are broken, so the build must not pass even if a
flaky test happens to succeed on the second attempt. The rerun exists solely
for diagnostic output.
- Docker builds must complete in under 5 minutes.
- `make check` must not modify any files in the repo. Tests may use temporary
directories.
- `main` must always pass `make check`, no exceptions.
- Never commit secrets. `.env` files, credentials, API keys, and private keys
must be in `.gitignore`. No exceptions.
- `.gitignore` should be comprehensive from the start: OS files (`.DS_Store`),
editor files (`.swp`, `*~`), language build artifacts, and `node_modules/`.
Fetch the standard `.gitignore` from
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.gitignore` when setting up
a new repo.
- **No build artifacts in version control.** Code-derived data (compiled
bundles, minified output, generated assets) must never be committed to the
repository if it can be avoided. The build process (e.g. Dockerfile, Makefile)
should generate these at build time. Notable exception: Go protobuf generated
files (`.pb.go`) ARE committed because repos need to work with `go get`, which
downloads code but does not execute code generation.
- Never use `git add -A` or `git add .`. Always stage files explicitly by name.
- Never force-push to `main`.
- Make all changes on a feature branch. You can do whatever you want on a
feature branch.
- `.golangci.yml` is standardized and must _NEVER_ be modified by an agent, only
manually by the user. Fetch from
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.golangci.yml`.
- When pinning images or packages by hash, add a comment above the reference
with the version and date (YYYY-MM-DD).
- Use `yarn`, not `npm`.
- Write all dates as YYYY-MM-DD (ISO 8601).
- Simple projects should be configured with environment variables.
- Dockerized web services listen on port 8080 by default, overridable with
`PORT`.
- **HTTP/web services must be hardened for production internet exposure before
tagging 1.0.** This means full compliance with security best practices
including, without limitation, all of the following:
- **Security headers** on every response:
- `Strict-Transport-Security` (HSTS) with `max-age` of at least one year
and `includeSubDomains`.
- `Content-Security-Policy` (CSP) with a restrictive default policy
(`default-src 'self'` as a baseline, tightened per-resource as
needed). Never use `unsafe-inline` or `unsafe-eval` unless
unavoidable, and document the reason.
- `X-Frame-Options: DENY` (or `SAMEORIGIN` if framing is required).
Prefer the `frame-ancestors` CSP directive as the primary control.
- `X-Content-Type-Options: nosniff`.
- `Referrer-Policy: strict-origin-when-cross-origin` (or stricter).
- `Permissions-Policy` restricting access to browser features the
application does not use (camera, microphone, geolocation, etc.).
- **Request and response limits:**
- Maximum request body size enforced on all endpoints (e.g. Go
`http.MaxBytesReader`). Choose a sane default per-route; never accept
unbounded input.
- Maximum response body size where applicable (e.g. paginated APIs).
- `ReadTimeout` and `ReadHeaderTimeout` on the `http.Server` to defend
against slowloris attacks.
- `WriteTimeout` on the `http.Server`.
- `IdleTimeout` on the `http.Server`.
- Per-handler execution time limits via `context.WithTimeout` or
chi/stdlib `middleware.Timeout`.
- **Authentication and session security:**
- Rate limiting on password-based authentication endpoints. API keys are
high-entropy and not susceptible to brute force, so they are exempt.
- CSRF tokens on all state-mutating HTML forms. API endpoints
authenticated via `Authorization` header (Bearer token, API key) are
exempt because the browser does not attach these automatically.
- Passwords stored using bcrypt, scrypt, or argon2 — never plain-text,
MD5, or SHA.
- Session cookies set with `HttpOnly`, `Secure`, and `SameSite=Lax` (or
`Strict`) attributes.
- **Reverse proxy awareness:**
- True client IP detection when behind a reverse proxy
(`X-Forwarded-For`, `X-Real-IP`). The application must accept
forwarded headers only from a configured set of trusted proxy
addresses — never trust `X-Forwarded-For` unconditionally.
- **CORS:**
- Authenticated endpoints must restrict `Access-Control-Allow-Origin` to
an explicit allowlist of known origins. Wildcard (`*`) is acceptable
only for public, unauthenticated read-only APIs.
- **Error handling:**
- Internal errors must never leak stack traces, SQL queries, file paths,
or other implementation details to the client. Return generic error
messages in production; detailed errors only when `DEBUG` is enabled.
- **TLS:**
- Services never terminate TLS directly. They are always deployed behind
a TLS-terminating reverse proxy. The service itself listens on plain
HTTP. However, HSTS headers and `Secure` cookie flags must still be
set by the application so that the browser enforces HTTPS end-to-end.
This list is non-exhaustive. Apply defense-in-depth: if a standard security
hardening measure exists for HTTP services and is not listed here, it is
still expected. When in doubt, harden.
- `README.md` is the primary documentation. Required sections:
- **Description**: First line must include the project name, purpose,
category (web server, SPA, CLI tool, etc.), license, and author. Example:
"µPaaS is an MIT-licensed Go web application by @sneak that receives
git-frontend webhooks and deploys applications via Docker in realtime."
- **Getting Started**: Copy-pasteable install/usage code block.
- **Rationale**: Why does this exist?
- **Design**: How is the program structured?
- **TODO**: Update meticulously, even between commits. When planning, put
the todo list in the README so a new agent can pick up where the last one
left off.
- **License**: MIT, GPL, or WTFPL. Ask the user for new projects. Include a
`LICENSE` file in the repo root and a License section in the README.
- **Author**: [@sneak](https://sneak.berlin).
- First commit of a new repo should contain only `README.md`.
- Go module root: `sneak.berlin/go/<name>`. Always run `go mod tidy` before
committing.
- Use SemVer.
- Database migrations live in `internal/db/migrations/` and must be embedded in
the binary.
- `000_migration.sql` — contains ONLY the creation of the migrations
tracking table itself. Nothing else.
- `001_schema.sql` — the full application schema.
- **Pre-1.0.0:** never add additional migration files (002, 003, etc.).
There is no installed base to migrate. Edit `001_schema.sql` directly.
- **Post-1.0.0:** add new numbered migration files for each schema change.
Never edit existing migrations after release.
- All repos should have an `.editorconfig` enforcing the project's indentation
settings.
- **Claude Code repo memory is versioned in the repo**, not left only in
`~/.claude` on one machine. Each memory is one file at
`.claude/memory/<memory>.md`, and every memory file must be `@`-imported from
`.claude/CLAUDE.md` (one `- @memory/<memory>.md` list line per file; relative
import paths resolve against `.claude/`, and Claude Code expands the imports
into context at session launch). When adding a memory, add both the file and
its import line. A root `MEMORY.md` is a violation — Claude Code never
auto-loads it; split it into `.claude/memory/` files. Repos with no memories
yet need no `.claude/` scaffolding.
- Avoid putting files in the repo root unless necessary. Root should contain
only project-level config files (`README.md`, `Makefile`, `Dockerfile`,
`LICENSE`, `.gitignore`, `.editorconfig`, `REPO_POLICIES.md`, and
language-specific config). Everything else goes in a subdirectory. Canonical
subdirectory names:
- `bin/` — executable scripts and tools
- `cmd/` — Go command entrypoints
- `configs/` — configuration templates and examples
- `deploy/` — deployment manifests (k8s, compose, terraform)
- `docs/` — documentation and markdown (README.md stays in root)
- `internal/` — Go internal packages
- `internal/db/migrations/` — database migrations
- `pkg/` — Go library packages
- `share/` — systemd units, data files
- `static/` — static assets (images, fonts, etc.)
- `web/` — web frontend source
- When setting up a new repo, files from the `prompts` repo may be used as
templates. Fetch them from
`https://git.eeqj.de/sneak/prompts/raw/branch/main/<path>`.
- New repos must contain at minimum:
- `README.md`, `.git`, `.gitignore`, `.editorconfig`
- `LICENSE`, `REPO_POLICIES.md` (copy from the `prompts` repo)
- `Makefile`
- `Dockerfile`, `.dockerignore`
- `.gitea/workflows/check.yml`
- Go: `go.mod`, `go.sum`, `.golangci.yml`
- JS: `package.json`, `yarn.lock`, `.prettierrc`, `.prettierignore`
- Python: `pyproject.toml`
-14
View File
@@ -1,14 +0,0 @@
package cmd
import (
"github.com/spf13/cobra"
)
func init() {
rootCmd.AddCommand(generateCommand)
}
var generateCommand = &cobra.Command{
Use: "generate",
Short: "Generate things",
}
-21
View File
@@ -1,21 +0,0 @@
package cmd
import (
"fmt"
"runtime/debug"
"github.com/spf13/cobra"
)
func init() {
rootCmd.AddCommand(infoCmd)
}
var infoCmd = &cobra.Command{
Use: "info",
Short: "Print tool info",
Run: func(cmd *cobra.Command, args []string) {
info, _ := debug.ReadBuildInfo()
fmt.Printf("%#v\n", info)
},
}
-29
View File
@@ -1,29 +0,0 @@
package cmd
import (
"crypto/rand"
"fmt"
"math/big"
"github.com/spf13/cobra"
"sneak.berlin/x/pkg/bip39wordlist"
)
func init() {
generateCommand.AddCommand(passwordCmd)
}
var passwordCmd = &cobra.Command{
Use: "password",
Short: "Print a randomly generated password",
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("%s\n", randomPassword())
},
}
func randomPassword() string {
wl := bip39wordlist.WordListEnglish
count := len(wl)
r, err := rand.Int(rand.Reader, big.Int(count))
return wl[r]
}
-52
View File
@@ -1,52 +0,0 @@
package cmd
import (
"fmt"
"os"
"github.com/mitchellh/go-homedir"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var (
// Used for flags.
cfgFile string
userLicense string
rootCmd = &cobra.Command{
Use: "sn",
Short: "A shell utility program",
}
)
// Execute executes the root command.
func Execute() error {
return rootCmd.Execute()
}
func er(msg interface{}) {
fmt.Println("Error:", msg)
os.Exit(1)
}
func init() {
cobra.OnInitialize(initConfig)
}
func initConfig() {
home, err := homedir.Dir()
if err != nil {
er(err)
}
// Search config in home directory with name ".cobra" (without extension).
viper.AddConfigPath(home)
viper.SetConfigName(".sneak")
viper.AutomaticEnv()
if err := viper.ReadInConfig(); err == nil {
fmt.Println("Using config file:", viper.ConfigFileUsed())
}
}
+20
View File
@@ -0,0 +1,20 @@
// Command sutil is sneak's shell utility: small tools backed by the
// packages in sneak.berlin/go/x. Run `sutil --help` for the command list.
package main
import (
"os"
"sneak.berlin/go/x/internal/cli"
)
// version is the build version. The Makefile and Dockerfile set it at link
// time with -ldflags "-X main.version=...".
var version = "dev"
func main() {
err := cli.New(cli.Params{Version: version}).Execute()
if err != nil {
os.Exit(1)
}
}
-20
View File
@@ -1,20 +0,0 @@
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
func init() {
rootCmd.AddCommand(versionCmd)
}
var versionCmd = &cobra.Command{
Use: "version",
Short: "Print the version number of Hugo",
Long: `All software has versions. This is Hugo's`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("sn version")
},
}
+15 -6
View File
@@ -1,10 +1,19 @@
module sneak.berlin/x
module sneak.berlin/go/x
go 1.15
go 1.26.0
require (
github.com/mitchellh/go-homedir v1.1.0
github.com/spf13/cobra v1.0.0
github.com/spf13/viper v1.7.1
github.com/stretchr/testify v1.3.0
github.com/kavfixnel/words v1.0.0
github.com/spf13/cobra v1.10.2
github.com/stretchr/testify v1.12.1
github.com/tyler-smith/go-bip39 v1.1.0
golang.org/x/crypto v0.58.0
)
require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/spf13/pflag v1.0.9 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect
golang.org/x/sys v0.49.0 // indirect
golang.org/x/text v0.43.0 // indirect
)
+24 -306
View File
@@ -1,312 +1,30 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8=
github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk=
github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kavfixnel/words v1.0.0 h1:k7/KXUnLIVNCAlBLXhdq1GunnxXmQqglZooHA+NRzuQ=
github.com/kavfixnel/words v1.0.0/go.mod h1:i2LITxcnsJnKfT+ruhPGVwzdcRDnsgQTL9TIooX9XAU=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8=
github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.58.0 h1:COkYLr4k7nDI4r6QtMML2AfzeIswWHAJthLS6K/M/54=
golang.org/x/crypto v0.58.0/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0 h1:HyfiK1WMnHj5FXFXatD+Qs1A/xC2Run6RzeW1SyHxpc=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc=
golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
+82
View File
@@ -0,0 +1,82 @@
package cli
import (
"fmt"
"io"
"github.com/spf13/cobra"
"sneak.berlin/go/x/pkg/cbk"
)
// maxMnemonicInputBytes bounds how much of stdin `cbk derive` will read; a
// 24-word mnemonic is well under 300 bytes.
const maxMnemonicInputBytes = 64 << 10
// newCBKCommand returns `sutil cbk`, the parent of the crypto_box
// keypair commands.
func newCBKCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "cbk",
Short: "crypto_box keypairs derived from BIP39 mnemonics",
}
cmd.AddCommand(newCBKDeriveCommand())
return cmd
}
// newCBKDeriveCommand returns `sutil cbk derive`.
func newCBKDeriveCommand() *cobra.Command {
var publicOnly bool
cmd := &cobra.Command{
Use: "derive",
Short: "Derive a crypto_box keypair from a mnemonic read on stdin",
Long: `Reads a BIP39 mnemonic from stdin, in any letter case and with any
whitespace between words, and prints the crypto_box keypair derived from it
as hex. The same mnemonic always yields the same keypair.
The mnemonic is read from stdin rather than taken as an argument so that it
does not end up in shell history or process listings:
sutil generate mnemonic > mnemonic.txt
sutil cbk derive < mnemonic.txt`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
input := io.LimitReader(cmd.InOrStdin(), maxMnemonicInputBytes)
mnemonic, err := io.ReadAll(input)
if err != nil {
return fmt.Errorf("reading mnemonic from stdin: %w", err)
}
keypair, err := cbk.New(cbk.Params{Mnemonic: string(mnemonic)})
if err != nil {
return err
}
return writeKeypair(cmd.OutOrStdout(), keypair, publicOnly)
},
}
cmd.Flags().BoolVar(&publicOnly, "public", false, "print only the public key")
return cmd
}
// writeKeypair prints the keypair as labeled hex lines, or just the bare
// public key when publicOnly is set.
func writeKeypair(w io.Writer, keypair cbk.CryptoBoxKeypair, publicOnly bool) error {
if publicOnly {
_, err := fmt.Fprintln(w, keypair.PublicKeyHex())
return err
}
_, err := fmt.Fprintf(
w, "public: %s\nprivate: %s\n", keypair.PublicKeyHex(), keypair.PrivateKeyHex(),
)
return err
}
+39
View File
@@ -0,0 +1,39 @@
// Package cli implements the sutil command tree.
//
// The tree is built by New rather than assembled in init functions, so
// the package holds no global state and tests can construct it repeatedly
// with captured input and output.
package cli
import (
"github.com/spf13/cobra"
)
// Params configures New.
type Params struct {
// Version is the version string embedded at build time. It is shown
// by `sutil version`, `sutil --version`, and `sutil info`.
Version string
}
// New builds the root sutil command with every subcommand attached.
func New(p Params) *cobra.Command {
root := &cobra.Command{
Use: "sutil",
Short: "sneak's shell utility",
Long: `sutil is a small collection of shell utilities built on the packages in
sneak.berlin/go/x: fast pseudorandom byte streams, memorable passwords,
BIP39 mnemonics, and crypto_box keypairs derived from them.`,
Version: p.Version,
SilenceUsage: true,
}
root.AddCommand(
newVersionCommand(p),
newInfoCommand(p),
newGenerateCommand(),
newCBKCommand(),
)
return root
}
+177
View File
@@ -0,0 +1,177 @@
package cli_test
import (
"bytes"
"errors"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"sneak.berlin/go/x/internal/cli"
"sneak.berlin/go/x/internal/pwgen"
"sneak.berlin/go/x/pkg/bip39wordlist"
"sneak.berlin/go/x/pkg/cbk"
)
const (
testVersion = "v1.2.3-test"
// vectorRepeats is how many times "abandon" appears in the standard
// BIP39 test vector for all-zero 128-bit entropy, which ends with
// "about".
vectorRepeats = bip39wordlist.MinWordCount - 1
)
// vectorMnemonic returns the standard BIP39 test vector for all-zero
// 128-bit entropy.
func vectorMnemonic() string {
return strings.Repeat("abandon ", vectorRepeats) + "about"
}
func TestVersionCommand(t *testing.T) {
t.Parallel()
output, err := execute(t, "", "version")
require.NoError(t, err)
assert.Equal(t, testVersion+"\n", output)
}
func TestVersionFlag(t *testing.T) {
t.Parallel()
output, err := execute(t, "", "--version")
require.NoError(t, err)
assert.Contains(t, output, testVersion)
}
func TestInfoCommand(t *testing.T) {
t.Parallel()
output, err := execute(t, "", "info")
require.NoError(t, err)
assert.Contains(t, output, "version: "+testVersion+"\n")
assert.Contains(t, output, "go: go")
assert.Contains(t, output, "module: sneak.berlin/go/x\n")
}
func TestGenerateRandomSeededIsDeterministic(t *testing.T) {
t.Parallel()
first, err := execute(t, "", "generate", "random", "--count", "4096", "seed")
require.NoError(t, err)
assert.Len(t, first, 4096)
second, err := execute(t, "", "generate", "random", "--count", "4096", "seed")
require.NoError(t, err)
assert.Equal(t, first, second)
other, err := execute(t, "", "gen", "random", "-c", "4096", "other seed")
require.NoError(t, err)
assert.NotEqual(t, first, other)
}
func TestGenerateRandomUnseeded(t *testing.T) {
t.Parallel()
first, err := execute(t, "", "generate", "random", "--count", "1024")
require.NoError(t, err)
assert.Len(t, first, 1024)
second, err := execute(t, "", "generate", "random", "--count", "1024")
require.NoError(t, err)
assert.NotEqual(t, first, second)
}
func TestGenerateRandomRejectsExtraArguments(t *testing.T) {
t.Parallel()
_, err := execute(t, "", "generate", "random", "one", "two")
require.Error(t, err)
}
func TestGenerateMnemonic(t *testing.T) {
t.Parallel()
output, err := execute(t, "", "generate", "mnemonic")
require.NoError(t, err)
mnemonic := strings.TrimSuffix(output, "\n")
require.NoError(t, bip39wordlist.ValidateWordlist(mnemonic))
assert.Len(t, strings.Fields(mnemonic), bip39wordlist.MaxWordCount)
output, err = execute(t, "", "generate", "mnemonic", "--words", "12")
require.NoError(t, err)
assert.Len(t, strings.Fields(output), bip39wordlist.MinWordCount)
_, err = execute(t, "", "generate", "mnemonic", "--words", "13")
require.ErrorIs(t, err, bip39wordlist.ErrInvalidWordCount)
}
func TestGeneratePassword(t *testing.T) {
t.Parallel()
_, err := pwgen.New(pwgen.Params{Words: nil, MinWordLength: 0, MaxWordLength: 0})
if errors.Is(err, pwgen.ErrNoUsableWords) {
t.Skip("this system has no word list")
}
require.NoError(t, err)
output, err := execute(t, "", "generate", "password")
require.NoError(t, err)
password := strings.TrimSuffix(output, "\n")
assert.NotContains(t, password, "\n")
assert.GreaterOrEqual(t, len(password), 3*(pwgen.DefaultMinWordLength+1)+2)
assert.True(t, strings.ContainsAny(password, "0123456789"), password)
assert.True(t, strings.ContainsAny(password, pwgen.Delimiters), password)
}
func TestCBKDerive(t *testing.T) {
t.Parallel()
expected, err := cbk.FromMnemonic(vectorMnemonic())
require.NoError(t, err)
stdin := " " + strings.ToUpper(vectorMnemonic()) + "\n"
output, err := execute(t, stdin, "cbk", "derive")
require.NoError(t, err)
assert.Equal(t,
"public: "+expected.PublicKeyHex()+"\n"+
"private: "+expected.PrivateKeyHex()+"\n",
output,
)
output, err = execute(t, vectorMnemonic(), "cbk", "derive", "--public")
require.NoError(t, err)
assert.Equal(t, expected.PublicKeyHex()+"\n", output)
}
func TestCBKDeriveRejectsInvalidMnemonic(t *testing.T) {
t.Parallel()
_, err := execute(t, "not a mnemonic\n", "cbk", "derive")
require.ErrorIs(t, err, bip39wordlist.ErrInvalidWordCount)
}
// execute runs sutil with the given stdin and arguments and returns
// everything it wrote to stdout and stderr.
func execute(t *testing.T, stdin string, args ...string) (string, error) {
t.Helper()
root := cli.New(cli.Params{Version: testVersion})
var output bytes.Buffer
root.SetOut(&output)
root.SetErr(&output)
root.SetIn(strings.NewReader(stdin))
root.SetArgs(args)
err := root.Execute()
return output.String(), err
}
+157
View File
@@ -0,0 +1,157 @@
package cli
import (
"fmt"
"io"
"github.com/spf13/cobra"
"sneak.berlin/go/x/internal/pwgen"
"sneak.berlin/go/x/internal/randstream"
"sneak.berlin/go/x/pkg/bip39wordlist"
)
// streamChunkSize is the write size used by `generate random`. Large
// writes keep the per-syscall overhead negligible.
const streamChunkSize = 1 << 20
// newGenerateCommand returns `sutil generate`, the parent of everything
// that produces random output.
func newGenerateCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "generate",
Aliases: []string{"gen"},
Short: "Generate random bytes, passwords, and mnemonics",
}
cmd.AddCommand(
newGenerateRandomCommand(),
newGeneratePasswordCommand(),
newGenerateMnemonicCommand(),
)
return cmd
}
// newGenerateRandomCommand returns `sutil generate random [seedstr]`.
func newGenerateRandomCommand() *cobra.Command {
var byteCount int64
cmd := &cobra.Command{
Use: "random [seedstr]",
Short: "Write a fast stream of pseudorandom bytes to stdout",
Long: `Writes pseudorandom bytes to stdout until stopped, or until --count
bytes have been written.
With seedstr the stream is deterministic: the same seed always produces
the same bytes. Without it the stream is keyed from the operating
system's secure random source and is different every run.
This exists because /dev/urandom is slow.`,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
seed := ""
if len(args) == 1 {
seed = args[0]
}
stream, err := randstream.New(randstream.Params{Seed: seed})
if err != nil {
return err
}
return streamBytes(cmd.OutOrStdout(), stream, byteCount)
},
}
cmd.Flags().Int64VarP(
&byteCount, "count", "c", 0, "stop after this many bytes (0 = never)",
)
return cmd
}
// newGeneratePasswordCommand returns `sutil generate password`.
func newGeneratePasswordCommand() *cobra.Command {
return &cobra.Command{
Use: "password",
Aliases: []string{"pw"},
Short: "Print a memorable password built from system dictionary words",
Long: `Prints a password of the form
<word><digit><delim><word><digit><delim><word><digit>
for example marble7-sprout2-velvet9. The words come from the system word
list (/usr/share/dict/words), the delimiter is chosen at random from
` + pwgen.Delimiters + `
and is the same in both positions. The command panics if the system has
no usable word list.`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
generator, err := pwgen.New(pwgen.Params{})
if err != nil {
panic("sutil: cannot load the system word list: " + err.Error())
}
password, err := generator.Generate()
if err != nil {
return err
}
_, err = fmt.Fprintln(cmd.OutOrStdout(), password)
return err
},
}
}
// newGenerateMnemonicCommand returns `sutil generate mnemonic`.
func newGenerateMnemonicCommand() *cobra.Command {
var wordCount int
cmd := &cobra.Command{
Use: "mnemonic",
Short: "Print a new random BIP39 mnemonic phrase",
Long: `Prints a new BIP39 mnemonic phrase generated from crypto/rand. The
default of 24 words encodes 256 bits of entropy; 12 words encodes 128.`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
mnemonic, err := bip39wordlist.GenerateMnemonic(wordCount)
if err != nil {
return err
}
_, err = fmt.Fprintln(cmd.OutOrStdout(), mnemonic)
return err
},
}
cmd.Flags().IntVarP(
&wordCount, "words", "w", bip39wordlist.MaxWordCount,
"number of words: 12, 15, 18, 21, or 24",
)
return cmd
}
// streamBytes copies limit bytes (or everything, when limit is zero) from
// src to dst in streamChunkSize chunks.
func streamBytes(dst io.Writer, src io.Reader, limit int64) error {
if limit > 0 {
src = io.LimitReader(src, limit)
}
// Wrapping dst hides any ReadFrom method it has, so io.CopyBuffer
// cannot bypass the large buffer.
_, err := io.CopyBuffer(writerOnly{dst}, src, make([]byte, streamChunkSize))
return err
}
// writerOnly exposes only the Write method of the wrapped writer.
type writerOnly struct {
io.Writer
}
+54
View File
@@ -0,0 +1,54 @@
package cli
import (
"errors"
"fmt"
"io"
"runtime/debug"
"strings"
"github.com/spf13/cobra"
)
// ErrNoBuildInfo is returned by `sutil info` when the binary carries no
// Go build information, which only happens for binaries not built with
// module support.
var ErrNoBuildInfo = errors.New("sutil: no build information in this binary")
// newInfoCommand returns `sutil info`, which prints the build version, the
// Go toolchain version, the module path, and the VCS metadata recorded by
// the Go toolchain at build time.
func newInfoCommand(p Params) *cobra.Command {
return &cobra.Command{
Use: "info",
Short: "Print build information",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
return writeBuildInfo(cmd.OutOrStdout(), p.Version)
},
}
}
// writeBuildInfo writes one "key: value" line per fact to w.
func writeBuildInfo(w io.Writer, version string) error {
info, ok := debug.ReadBuildInfo()
if !ok {
return ErrNoBuildInfo
}
lines := []string{
"version: " + version,
"go: " + info.GoVersion,
"module: " + info.Main.Path,
}
for _, setting := range info.Settings {
if strings.HasPrefix(setting.Key, "vcs.") {
lines = append(lines, setting.Key+": "+setting.Value)
}
}
_, err := fmt.Fprintln(w, strings.Join(lines, "\n"))
return err
}
+22
View File
@@ -0,0 +1,22 @@
package cli
import (
"fmt"
"github.com/spf13/cobra"
)
// newVersionCommand returns `sutil version`, which prints the build
// version on its own line.
func newVersionCommand(p Params) *cobra.Command {
return &cobra.Command{
Use: "version",
Short: "Print the sutil version",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
_, err := fmt.Fprintln(cmd.OutOrStdout(), p.Version)
return err
},
}
}
+195
View File
@@ -0,0 +1,195 @@
// Package pwgen generates memorable passwords from the system dictionary.
// It backs `sutil generate password`.
//
// A password is three dictionary words, each followed by one decimal
// digit, joined by a delimiter that is chosen at random from Delimiters
// and used in both positions:
//
// <word><digit><delim><word><digit><delim><word><digit>
//
// for example "marble7-sprout2-velvet9".
//
// Words come from the operating system's word list (/usr/share/dict/words
// on macOS and most Unix systems) through github.com/kavfixnel/words,
// filtered to words made only of lowercase ASCII letters whose length is
// within the configured bounds. That drops proper nouns, abbreviations,
// and anything awkward to type. All random choices use crypto/rand.
package pwgen
import (
"crypto/rand"
"errors"
"fmt"
"math/big"
"strconv"
"strings"
"github.com/kavfixnel/words"
)
const (
// WordCount is the number of words in every generated password.
WordCount = 3
// Delimiters is the set of characters from which the delimiter that
// separates the words of a password is chosen.
Delimiters = ".-_|~+=:,!@#%^&*"
// DefaultMinWordLength is the shortest word used when
// Params.MinWordLength is zero.
DefaultMinWordLength = 4
// DefaultMaxWordLength is the longest word used when
// Params.MaxWordLength is zero.
DefaultMaxWordLength = 8
// digitCount is the number of decimal digits (0 through 9) one of
// which follows every word.
digitCount = 10
)
var (
// ErrNoUsableWords is returned by New when the word source yields no
// words within the length bounds. On a system without a word list,
// github.com/kavfixnel/words returns an empty list rather than an
// error, so this is also the error for "no system dictionary".
ErrNoUsableWords = errors.New("pwgen: no usable words in word list")
// ErrInvalidWordLength is returned by New when the word length bounds
// are negative or the minimum exceeds the maximum.
ErrInvalidWordLength = errors.New("pwgen: invalid word length bounds")
)
// Params configures New.
type Params struct {
// Words, when non-nil, is used instead of the system dictionary. It
// is filtered by the same length and character rules.
Words []string
// MinWordLength is the shortest word to use; zero means
// DefaultMinWordLength.
MinWordLength int
// MaxWordLength is the longest word to use; zero means
// DefaultMaxWordLength.
MaxWordLength int
}
// Generator produces passwords from a fixed set of usable words. It is
// safe for concurrent use.
type Generator struct {
words []string
}
// New loads and filters the word source described by p. It returns
// ErrNoUsableWords if no words survive filtering, which on a system
// without a word list is the normal outcome.
func New(p Params) (*Generator, error) {
minLength, maxLength := p.MinWordLength, p.MaxWordLength
if minLength == 0 {
minLength = DefaultMinWordLength
}
if maxLength == 0 {
maxLength = DefaultMaxWordLength
}
if minLength < 0 || maxLength < minLength {
return nil, fmt.Errorf(
"%w: min %d, max %d", ErrInvalidWordLength, minLength, maxLength,
)
}
source := p.Words
if source == nil {
systemWords, err := words.NewWordList(nil)
if err != nil {
return nil, fmt.Errorf("pwgen: reading system word list: %w", err)
}
source = systemWords
}
usable := filterWords(source, minLength, maxLength)
if len(usable) == 0 {
return nil, ErrNoUsableWords
}
return &Generator{words: usable}, nil
}
// UsableWords returns the number of distinct words the generator chooses
// from, which determines the strength of its passwords.
func (g *Generator) UsableWords() int {
return len(g.words)
}
// Generate returns a new random password.
func (g *Generator) Generate() (string, error) {
delimiterIndex, err := randomIndex(len(Delimiters))
if err != nil {
return "", err
}
delimiter := Delimiters[delimiterIndex]
var password strings.Builder
for i := range WordCount {
if i > 0 {
password.WriteByte(delimiter)
}
wordIndex, err := randomIndex(len(g.words))
if err != nil {
return "", err
}
digit, err := randomIndex(digitCount)
if err != nil {
return "", err
}
password.WriteString(g.words[wordIndex])
password.WriteString(strconv.Itoa(digit))
}
return password.String(), nil
}
// filterWords returns the entries of source that isUsableWord accepts.
func filterWords(source []string, minLength, maxLength int) []string {
usable := make([]string, 0, len(source))
for _, word := range source {
if isUsableWord(word, minLength, maxLength) {
usable = append(usable, word)
}
}
return usable
}
// isUsableWord reports whether word is made only of lowercase ASCII
// letters and has a length within the bounds.
func isUsableWord(word string, minLength, maxLength int) bool {
if len(word) < minLength || len(word) > maxLength {
return false
}
return !strings.ContainsFunc(word, func(r rune) bool {
return r < 'a' || r > 'z'
})
}
// randomIndex returns a uniformly random integer in [0, n) from
// crypto/rand.
func randomIndex(n int) (int, error) {
value, err := rand.Int(rand.Reader, big.NewInt(int64(n)))
if err != nil {
return 0, fmt.Errorf("pwgen: reading from crypto/rand: %w", err)
}
return int(value.Int64()), nil
}
+176
View File
@@ -0,0 +1,176 @@
package pwgen_test
import (
"errors"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"sneak.berlin/go/x/internal/pwgen"
)
const generateRounds = 300
// testWords mixes usable words with entries the filter must drop:
// capitalized, too short, too long, punctuation, digits, and non-ASCII.
func testWords() []string {
return []string{
"apple", "banana", "cherry", "grape", "lemon", "mango", "melon", "peach",
"Capital", "ab", "abc", "toolongwordhere", "has-dash", "num3er", "éclair",
}
}
func usableTestWords() map[string]bool {
return map[string]bool{
"apple": true, "banana": true, "cherry": true, "grape": true,
"lemon": true, "mango": true, "melon": true, "peach": true,
}
}
func TestNewFiltersWords(t *testing.T) {
t.Parallel()
generator, err := pwgen.New(pwgen.Params{Words: testWords()})
require.NoError(t, err)
assert.Equal(t, len(usableTestWords()), generator.UsableWords())
}
func TestNewHonorsLengthBounds(t *testing.T) {
t.Parallel()
generator, err := pwgen.New(pwgen.Params{
Words: testWords(),
MinWordLength: 2,
MaxWordLength: 3,
})
require.NoError(t, err)
assert.Equal(t, 2, generator.UsableWords(), `only "ab" and "abc" fit`)
}
func TestNewRejectsNoUsableWords(t *testing.T) {
t.Parallel()
_, err := pwgen.New(pwgen.Params{Words: []string{"Capital", "ab"}})
require.ErrorIs(t, err, pwgen.ErrNoUsableWords)
_, err = pwgen.New(pwgen.Params{Words: []string{}})
require.ErrorIs(t, err, pwgen.ErrNoUsableWords)
}
func TestNewRejectsInvalidBounds(t *testing.T) {
t.Parallel()
_, err := pwgen.New(pwgen.Params{
Words: testWords(),
MinWordLength: 9,
MaxWordLength: 4,
})
require.ErrorIs(t, err, pwgen.ErrInvalidWordLength)
_, err = pwgen.New(pwgen.Params{Words: testWords(), MinWordLength: -1})
require.ErrorIs(t, err, pwgen.ErrInvalidWordLength)
}
func TestGenerateFormat(t *testing.T) {
t.Parallel()
generator, err := pwgen.New(pwgen.Params{Words: testWords()})
require.NoError(t, err)
usable := usableTestWords()
for range generateRounds {
password, err := generator.Generate()
require.NoError(t, err)
words, digits, delimiter := parsePassword(t, password)
assert.Contains(t, pwgen.Delimiters, string(delimiter))
assert.Len(t, words, pwgen.WordCount)
for _, word := range words {
assert.True(t, usable[word], "unexpected word %q in %q", word, password)
}
for _, digit := range digits {
assert.True(t, digit >= '0' && digit <= '9', "digit in %q", password)
}
}
}
func TestGenerateVariesDelimitersAndDigits(t *testing.T) {
t.Parallel()
generator, err := pwgen.New(pwgen.Params{Words: testWords()})
require.NoError(t, err)
delimitersSeen := map[byte]bool{}
digitsSeen := map[byte]bool{}
passwordsSeen := map[string]bool{}
for range generateRounds {
password, err := generator.Generate()
require.NoError(t, err)
_, digits, delimiter := parsePassword(t, password)
delimitersSeen[delimiter] = true
passwordsSeen[password] = true
for _, digit := range digits {
digitsSeen[digit] = true
}
}
assert.Greater(t, len(delimitersSeen), 1, "delimiter should vary")
assert.Greater(t, len(digitsSeen), 1, "digits should vary")
assert.Greater(t, len(passwordsSeen), generateRounds/2, "passwords repeat")
}
func TestNewWithSystemDictionary(t *testing.T) {
t.Parallel()
generator, err := pwgen.New(pwgen.Params{})
if errors.Is(err, pwgen.ErrNoUsableWords) {
t.Skip("this system has no word list")
}
require.NoError(t, err)
assert.Greater(t, generator.UsableWords(), 1000)
password, err := generator.Generate()
require.NoError(t, err)
parsePassword(t, password)
}
// parsePassword splits a password of the form
// <word><digit><delim><word><digit><delim><word><digit> into its words,
// digits, and the delimiter, failing the test if it is not of that shape.
func parsePassword(t *testing.T, password string) ([]string, []byte, byte) {
t.Helper()
delimiterIndex := strings.IndexFunc(password, func(r rune) bool {
return (r < 'a' || r > 'z') && (r < '0' || r > '9')
})
require.NotEqual(t, -1, delimiterIndex, "no delimiter in %q", password)
delimiter := password[delimiterIndex]
parts := strings.Split(password, string(delimiter))
require.Len(t, parts, pwgen.WordCount, "wrong shape: %q", password)
words := make([]string, 0, len(parts))
digits := make([]byte, 0, len(parts))
for _, part := range parts {
require.GreaterOrEqual(t, len(part), 2, "short part in %q", password)
words = append(words, part[:len(part)-1])
digits = append(digits, part[len(part)-1])
}
return words, digits, delimiter
}
+76
View File
@@ -0,0 +1,76 @@
// Package randstream provides a fast, unbounded stream of pseudorandom
// bytes as an io.Reader. It backs `sutil generate random`.
//
// It exists because reading /dev/urandom is slow: operating system random
// devices are built for small requests of key material, not for filling
// disks or producing gigabytes of test data. A Reader instead expands a
// 32-byte key with the ChaCha8 generator from math/rand/v2, which runs at
// close to memory bandwidth and never blocks.
//
// When Params.Seed is set, the key is SHA-256 of the seed string and the
// stream is fully deterministic: the same seed always produces the same
// bytes, which makes test data reproducible. When Seed is empty, the key
// is read from crypto/rand (the operating system's secure random source,
// /dev/urandom or its equivalent) and the stream is unpredictable.
//
// The output is not meant to be used as cryptographic key material; use
// crypto/rand for that.
package randstream
import (
"crypto/rand"
"crypto/sha256"
"fmt"
mathrand "math/rand/v2"
)
// KeySize is the size in bytes of the key that seeds the stream.
const KeySize = 32
// Params configures New.
type Params struct {
// Seed, when non-empty, makes the stream deterministic: the same seed
// always yields the same byte sequence. When empty, the stream is
// keyed from crypto/rand and differs on every call to New.
Seed string
}
// Reader is an unbounded stream of pseudorandom bytes. Read never returns
// an error and never returns fewer bytes than requested. A Reader is not
// safe for concurrent use.
type Reader struct {
generator *mathrand.ChaCha8
}
// New returns a Reader configured by p.
func New(p Params) (*Reader, error) {
key, err := keyFromParams(p)
if err != nil {
return nil, err
}
return &Reader{generator: mathrand.NewChaCha8(key)}, nil
}
// Read fills b with pseudorandom bytes. It always returns len(b) and a nil
// error.
func (r *Reader) Read(b []byte) (int, error) {
return r.generator.Read(b)
}
// keyFromParams derives the ChaCha8 key: SHA-256 of the seed string, or
// fresh bytes from crypto/rand when no seed is given.
func keyFromParams(p Params) ([KeySize]byte, error) {
if p.Seed != "" {
return sha256.Sum256([]byte(p.Seed)), nil
}
var key [KeySize]byte
_, err := rand.Read(key[:])
if err != nil {
return key, fmt.Errorf("randstream: reading key from crypto/rand: %w", err)
}
return key, nil
}
+111
View File
@@ -0,0 +1,111 @@
package randstream_test
import (
"io"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"sneak.berlin/go/x/internal/randstream"
)
const (
sampleSize = 1 << 20
helloSeed = "hello"
)
func TestSeededStreamIsDeterministic(t *testing.T) {
t.Parallel()
first := readSample(t, randstream.Params{Seed: helloSeed}, sampleSize)
second := readSample(t, randstream.Params{Seed: helloSeed}, sampleSize)
assert.Equal(t, first, second)
}
func TestDifferentSeedsDiffer(t *testing.T) {
t.Parallel()
hello := readSample(t, randstream.Params{Seed: helloSeed}, sampleSize)
world := readSample(t, randstream.Params{Seed: "world"}, sampleSize)
assert.NotEqual(t, hello, world)
}
func TestUnseededStreamsDiffer(t *testing.T) {
t.Parallel()
first := readSample(t, randstream.Params{Seed: ""}, sampleSize)
second := readSample(t, randstream.Params{Seed: ""}, sampleSize)
assert.NotEqual(t, first, second)
}
func TestReadAlwaysFillsBuffer(t *testing.T) {
t.Parallel()
stream, err := randstream.New(randstream.Params{Seed: "fill"})
require.NoError(t, err)
for _, size := range []int{0, 1, 7, 64, 65, 4096, sampleSize} {
buffer := make([]byte, size)
n, err := stream.Read(buffer)
require.NoError(t, err)
assert.Equal(t, size, n)
}
}
func TestStreamIsContinuousAcrossReads(t *testing.T) {
t.Parallel()
const headSize, tailSize = 300, 700
whole := readSample(t, randstream.Params{Seed: "continuous"}, headSize+tailSize)
stream, err := randstream.New(randstream.Params{Seed: "continuous"})
require.NoError(t, err)
head := make([]byte, headSize)
tail := make([]byte, tailSize)
_, err = io.ReadFull(stream, head)
require.NoError(t, err)
_, err = io.ReadFull(stream, tail)
require.NoError(t, err)
assert.Equal(t, whole[:headSize], head)
assert.Equal(t, whole[headSize:], tail)
}
func TestOutputIsNotTriviallyPatterned(t *testing.T) {
t.Parallel()
sample := readSample(t, randstream.Params{Seed: "entropy"}, sampleSize)
var seen [256]bool
for _, b := range sample {
seen[b] = true
}
for value, wasSeen := range seen {
assert.True(t, wasSeen, "byte value %d never appeared", value)
}
}
// readSample reads size bytes from a new Reader configured by p.
func readSample(t *testing.T, p randstream.Params, size int) []byte {
t.Helper()
stream, err := randstream.New(p)
require.NoError(t, err)
sample := make([]byte, size)
_, err = io.ReadFull(stream, sample)
require.NoError(t, err)
return sample
}
-9
View File
@@ -1,9 +0,0 @@
package main
import (
"sneak.berlin/x/cmd"
)
func main() {
cmd.Execute()
}
+8
View File
@@ -0,0 +1,8 @@
{
"name": "x",
"private": true,
"description": "prettier is used by `make fmt` and `make fmt-check` to format markdown",
"devDependencies": {
"prettier": "3.9.10"
}
}
File diff suppressed because it is too large. Load diff
-17
View File
@@ -1,17 +0,0 @@
package bip39wordlist_test
import (
"testing"
"github.com/stretchr/testify/assert"
"sneak.berlin/x/pkg/bip39wordlist"
)
func TestLength(t *testing.T) {
assert := assert.New(t)
wl := bip39wordlist.Wordlist
assert.Equal(2048, len(wl), "should have correct number of words")
assert.Equal(wl[0], "abandon", "first word should be correct")
assert.Equal(wl[len(wl)-1], "zoo", "last word should be correct")
}
File diff suppressed because it is too large. Load diff
+118
View File
@@ -0,0 +1,118 @@
package bip39wordlist
import (
"errors"
"fmt"
"strings"
"github.com/tyler-smith/go-bip39"
)
const (
// MinWordCount is the number of words in the shortest valid mnemonic,
// which encodes 128 bits of entropy.
MinWordCount = 12
// MaxWordCount is the number of words in the longest valid mnemonic,
// which encodes 256 bits of entropy.
MaxWordCount = 24
// WordCountStep is the granularity of valid word counts. Every valid
// mnemonic has a multiple of three words, because each group of three
// words carries 32 bits of entropy plus one checksum bit.
WordCountStep = 3
// entropyBitsPerWordGroup is the entropy carried by each group of
// WordCountStep words.
entropyBitsPerWordGroup = 32
)
var (
// ErrInvalidWordCount is returned when a mnemonic does not have 12,
// 15, 18, 21, or 24 words.
ErrInvalidWordCount = errors.New(
"bip39wordlist: mnemonic must have 12, 15, 18, 21, or 24 words",
)
// ErrUnknownWord is returned when a mnemonic contains a word that is
// not in the wordlist. The returned error names the word and its
// one-based position.
ErrUnknownWord = errors.New("bip39wordlist: word is not in the wordlist")
// ErrInvalidChecksum is returned when every word is valid but the
// checksum bits encoded in the final word do not match the entropy.
ErrInvalidChecksum = errors.New("bip39wordlist: mnemonic checksum mismatch")
)
// ValidWordCount reports whether n is a valid number of words for a
// mnemonic: a multiple of three from 12 to 24 inclusive.
func ValidWordCount(n int) bool {
return n >= MinWordCount && n <= MaxWordCount && n%WordCountStep == 0
}
// ValidateWordlist checks that mnemonic is a valid BIP39 English mnemonic:
// a valid number of words, every word in the wordlist, and a correct
// checksum. Letter case and the amount of whitespace between words are
// ignored, so any phrase ValidateWordlist accepts can be canonicalized with
// NormalizeWordlist. It returns nil for a valid mnemonic and otherwise an
// error wrapping ErrInvalidWordCount, ErrUnknownWord, or
// ErrInvalidChecksum.
func ValidateWordlist(mnemonic string) error {
words := splitWords(mnemonic)
if !ValidWordCount(len(words)) {
return fmt.Errorf("%w: got %d", ErrInvalidWordCount, len(words))
}
for i, word := range words {
if !Contains(word) {
return fmt.Errorf("%w: %q at position %d", ErrUnknownWord, word, i+1)
}
}
if !bip39.IsMnemonicValid(strings.Join(words, " ")) {
return ErrInvalidChecksum
}
return nil
}
// NormalizeWordlist returns mnemonic in canonical form: lowercase, with
// the words separated by exactly one space and no leading or trailing
// whitespace. It refuses invalid input: if mnemonic fails
// ValidateWordlist, that error is returned and the string is empty.
func NormalizeWordlist(mnemonic string) (string, error) {
err := ValidateWordlist(mnemonic)
if err != nil {
return "", err
}
return strings.Join(splitWords(mnemonic), " "), nil
}
// GenerateMnemonic returns a new random mnemonic of wordCount words, using
// entropy from crypto/rand. wordCount must satisfy ValidWordCount.
func GenerateMnemonic(wordCount int) (string, error) {
if !ValidWordCount(wordCount) {
return "", fmt.Errorf("%w: got %d", ErrInvalidWordCount, wordCount)
}
entropyBits := wordCount / WordCountStep * entropyBitsPerWordGroup
entropy, err := bip39.NewEntropy(entropyBits)
if err != nil {
return "", fmt.Errorf("bip39wordlist: generating entropy: %w", err)
}
mnemonic, err := bip39.NewMnemonic(entropy)
if err != nil {
return "", fmt.Errorf("bip39wordlist: encoding mnemonic: %w", err)
}
return mnemonic, nil
}
// splitWords lowercases mnemonic and splits it on any run of whitespace.
func splitWords(mnemonic string) []string {
return strings.Fields(strings.ToLower(mnemonic))
}
+143
View File
@@ -0,0 +1,143 @@
package bip39wordlist_test
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"sneak.berlin/go/x/pkg/bip39wordlist"
)
// vectorRepeats is how many times "abandon" appears in the standard BIP39
// test vector for all-zero 128-bit entropy, which ends with "about".
const vectorRepeats = bip39wordlist.MinWordCount - 1
// vectorMnemonic returns the standard BIP39 test vector for all-zero
// 128-bit entropy.
func vectorMnemonic() string {
return strings.Repeat("abandon ", vectorRepeats) + "about"
}
// messyVectorMnemonic returns vectorMnemonic with mixed case and irregular
// whitespace; it must normalize to vectorMnemonic.
func messyVectorMnemonic() string {
return " Abandon ABANDON\t" + strings.Repeat("abandon ", 4) + "\n" +
strings.Repeat("abandon ", 5) + "About \n"
}
func TestValidWordCount(t *testing.T) {
t.Parallel()
for _, n := range []int{12, 15, 18, 21, 24} {
assert.True(t, bip39wordlist.ValidWordCount(n), "%d words", n)
}
for _, n := range []int{0, 1, 9, 11, 13, 14, 16, 23, 25, 27, 36} {
assert.False(t, bip39wordlist.ValidWordCount(n), "%d words", n)
}
}
func TestValidateWordlistAcceptsValidMnemonics(t *testing.T) {
t.Parallel()
require.NoError(t, bip39wordlist.ValidateWordlist(vectorMnemonic()))
require.NoError(t, bip39wordlist.ValidateWordlist(messyVectorMnemonic()))
}
func TestValidateWordlistRejectsWrongWordCount(t *testing.T) {
t.Parallel()
err := bip39wordlist.ValidateWordlist("")
require.ErrorIs(t, err, bip39wordlist.ErrInvalidWordCount)
err = bip39wordlist.ValidateWordlist(vectorMnemonic() + " abandon")
require.ErrorIs(t, err, bip39wordlist.ErrInvalidWordCount)
assert.Contains(t, err.Error(), "got 13")
}
func TestValidateWordlistRejectsUnknownWord(t *testing.T) {
t.Parallel()
words := strings.Fields(vectorMnemonic())
words[4] = "bogus"
err := bip39wordlist.ValidateWordlist(strings.Join(words, " "))
require.ErrorIs(t, err, bip39wordlist.ErrUnknownWord)
assert.Contains(t, err.Error(), `"bogus" at position 5`)
}
func TestValidateWordlistRejectsBadChecksum(t *testing.T) {
t.Parallel()
// Twelve times "abandon" has valid words and count but the checksum
// bits in the last word are wrong.
allAbandon := strings.Repeat("abandon ", bip39wordlist.MinWordCount)
err := bip39wordlist.ValidateWordlist(allAbandon)
require.ErrorIs(t, err, bip39wordlist.ErrInvalidChecksum)
}
func TestNormalizeWordlistCanonicalizes(t *testing.T) {
t.Parallel()
normalized, err := bip39wordlist.NormalizeWordlist(messyVectorMnemonic())
require.NoError(t, err)
assert.Equal(t, vectorMnemonic(), normalized)
normalized, err = bip39wordlist.NormalizeWordlist(vectorMnemonic())
require.NoError(t, err)
assert.Equal(t, vectorMnemonic(), normalized)
}
func TestNormalizeWordlistRefusesInvalid(t *testing.T) {
t.Parallel()
normalized, err := bip39wordlist.NormalizeWordlist("Abandon Bogus about")
require.ErrorIs(t, err, bip39wordlist.ErrInvalidWordCount)
assert.Empty(t, normalized)
normalized, err = bip39wordlist.NormalizeWordlist(
strings.Repeat("abandon ", bip39wordlist.MinWordCount),
)
require.ErrorIs(t, err, bip39wordlist.ErrInvalidChecksum)
assert.Empty(t, normalized)
}
func TestGenerateMnemonicProducesValidMnemonics(t *testing.T) {
t.Parallel()
for _, wordCount := range []int{12, 15, 18, 21, 24} {
mnemonic, err := bip39wordlist.GenerateMnemonic(wordCount)
require.NoError(t, err)
require.NoError(t, bip39wordlist.ValidateWordlist(mnemonic))
assert.Len(t, strings.Fields(mnemonic), wordCount)
normalized, err := bip39wordlist.NormalizeWordlist(mnemonic)
require.NoError(t, err)
assert.Equal(t, mnemonic, normalized, "generated mnemonic is canonical")
}
}
func TestGenerateMnemonicIsRandom(t *testing.T) {
t.Parallel()
first, err := bip39wordlist.GenerateMnemonic(bip39wordlist.MaxWordCount)
require.NoError(t, err)
second, err := bip39wordlist.GenerateMnemonic(bip39wordlist.MaxWordCount)
require.NoError(t, err)
assert.NotEqual(t, first, second)
}
func TestGenerateMnemonicRejectsInvalidWordCount(t *testing.T) {
t.Parallel()
for _, wordCount := range []int{0, 11, 13, 25} {
mnemonic, err := bip39wordlist.GenerateMnemonic(wordCount)
require.ErrorIs(t, err, bip39wordlist.ErrInvalidWordCount)
assert.Empty(t, mnemonic)
}
}
+42
View File
@@ -0,0 +1,42 @@
// Package bip39wordlist provides the BIP39 English wordlist and helpers for
// validating, normalizing, and generating BIP39 mnemonic phrases.
//
// The wordlist is embedded in the binary from english.txt and parsed once
// at package initialization. The mnemonic helpers accept phrases in any
// letter case and with any whitespace between words; NormalizeWordlist
// returns the canonical lowercase, single-space-separated form that BIP39
// seed derivation requires, and refuses anything that is not a valid
// mnemonic.
package bip39wordlist
import (
_ "embed"
"slices"
"strings"
)
// WordlistSize is the number of words in a BIP39 wordlist.
const WordlistSize = 2048
//go:embed english.txt
var englishWordlistData string
// WordlistEnglish is the BIP39 English wordlist, one entry per word, in
// the canonical (sorted) BIP39 order. Callers must not modify it.
//
//nolint:gochecknoglobals // static lookup table parsed once from embedded data
var WordlistEnglish = strings.Fields(englishWordlistData)
// Wordlist is the default wordlist, which is WordlistEnglish. Callers must
// not modify it.
//
//nolint:gochecknoglobals // static lookup table, alias of WordlistEnglish
var Wordlist = WordlistEnglish
// Contains reports whether word is in the English wordlist. The comparison
// is exact, so word must already be lowercase.
func Contains(word string) bool {
_, found := slices.BinarySearch(WordlistEnglish, word)
return found
}
+45
View File
@@ -0,0 +1,45 @@
package bip39wordlist_test
import (
"slices"
"testing"
"github.com/stretchr/testify/assert"
"github.com/tyler-smith/go-bip39/wordlists"
"sneak.berlin/go/x/pkg/bip39wordlist"
)
func TestWordlistEnglishShape(t *testing.T) {
t.Parallel()
wordlist := bip39wordlist.WordlistEnglish
assert.Len(t, wordlist, bip39wordlist.WordlistSize)
assert.Equal(t, "abandon", wordlist[0])
assert.Equal(t, "zoo", wordlist[len(wordlist)-1])
assert.True(t, slices.IsSorted(wordlist), "wordlist must be sorted")
}
func TestWordlistEnglishMatchesReferenceImplementation(t *testing.T) {
t.Parallel()
assert.Equal(t, wordlists.English, bip39wordlist.WordlistEnglish)
}
func TestWordlistIsEnglish(t *testing.T) {
t.Parallel()
assert.Equal(t, bip39wordlist.WordlistEnglish, bip39wordlist.Wordlist)
}
func TestContains(t *testing.T) {
t.Parallel()
assert.True(t, bip39wordlist.Contains("abandon"))
assert.True(t, bip39wordlist.Contains("zoo"))
assert.True(t, bip39wordlist.Contains("legal"))
assert.False(t, bip39wordlist.Contains("Abandon"), "lookup is case sensitive")
assert.False(t, bip39wordlist.Contains("notaword"))
assert.False(t, bip39wordlist.Contains(""))
}
+223
View File
@@ -0,0 +1,223 @@
// Package cbk derives NaCl crypto_box keypairs (Curve25519, XSalsa20,
// Poly1305) deterministically from BIP39 mnemonic phrases, and seals and
// opens messages with them.
//
// # Key derivation
//
// The same mnemonic and passphrase always produce the same keypair:
//
// 1. The mnemonic is validated and canonicalized with
// bip39wordlist.NormalizeWordlist (lowercase, single spaces).
// 2. The standard 64-byte BIP39 seed is computed from the canonical
// mnemonic and the optional passphrase with PBKDF2-HMAC-SHA512 (2048
// rounds, salt "mnemonic" + passphrase), exactly as a BIP39 wallet
// would.
// 3. The 32-byte crypto_box private key is derived from that seed with
// HKDF-SHA256, using no salt and DerivationInfo as the info string.
// 4. The public key is the Curve25519 scalar multiplication of the
// private key with the base point, via golang.org/x/crypto/nacl/box.
//
// # Sealing
//
// Seal and Open wrap box.Seal and box.Open. A sealed message is the
// 24-byte random nonce followed by the box ciphertext, so it can be stored
// or transmitted as a single opaque byte string.
package cbk
import (
"bytes"
"crypto/hkdf"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"github.com/tyler-smith/go-bip39"
"golang.org/x/crypto/nacl/box"
"sneak.berlin/go/x/pkg/bip39wordlist"
)
const (
// KeySize is the length in bytes of a crypto_box public or private key.
KeySize = 32
// NonceSize is the length in bytes of the nonce that prefixes every
// sealed message.
NonceSize = 24
// DerivationInfo is the HKDF info string that binds derived keys to
// this package. Changing it changes every derived key.
DerivationInfo = "sneak.berlin/go/x/pkg/cbk crypto_box v1"
hexKeyLength = KeySize * 2
)
var (
// ErrInvalidKeyHex is returned by KeyFromHex for input that is not
// exactly 64 hexadecimal characters.
ErrInvalidKeyHex = errors.New("cbk: key must be 64 hexadecimal characters")
// ErrSealedTooShort is returned by Open when the input is shorter than
// the nonce that must prefix it.
ErrSealedTooShort = errors.New("cbk: sealed message is shorter than a nonce")
// ErrOpenFailed is returned by Open when authentication fails: the
// message was sealed for a different keypair, by a different sender,
// or has been modified.
ErrOpenFailed = errors.New("cbk: open failed: wrong keys or modified message")
)
// CryptoBoxKey is a 32-byte crypto_box key, either public or private.
type CryptoBoxKey [KeySize]byte
// KeyFromHex parses a key from its 64-character hexadecimal encoding, as
// produced by Hex.
func KeyFromHex(encoded string) (CryptoBoxKey, error) {
var key CryptoBoxKey
if len(encoded) != hexKeyLength {
return key, fmt.Errorf(
"%w: got %d characters", ErrInvalidKeyHex, len(encoded),
)
}
decoded, err := hex.DecodeString(encoded)
if err != nil {
return key, fmt.Errorf("%w: %w", ErrInvalidKeyHex, err)
}
copy(key[:], decoded)
return key, nil
}
// Bytes returns a copy of the key as a byte slice.
func (k CryptoBoxKey) Bytes() []byte {
return k[:]
}
// Hex returns the key as 64 lowercase hexadecimal characters.
func (k CryptoBoxKey) Hex() string {
return hex.EncodeToString(k[:])
}
// Params configures New.
type Params struct {
// Mnemonic is the BIP39 mnemonic phrase. Any letter case and any
// whitespace between words is accepted; it is normalized before use.
Mnemonic string
// Passphrase is the optional BIP39 passphrase (sometimes called the
// "25th word"). It is used verbatim: it is case sensitive and is not
// trimmed. An empty passphrase is the common case.
Passphrase string
}
// CryptoBoxKeypair is a crypto_box public and private key pair.
type CryptoBoxKeypair struct {
PublicKey CryptoBoxKey
PrivateKey CryptoBoxKey
}
// New derives the keypair for the mnemonic and passphrase in p. If the
// mnemonic is not valid it returns an error wrapping one of the
// bip39wordlist sentinel errors.
func New(p Params) (CryptoBoxKeypair, error) {
mnemonic, err := bip39wordlist.NormalizeWordlist(p.Mnemonic)
if err != nil {
return CryptoBoxKeypair{}, err
}
seed := bip39.NewSeed(mnemonic, p.Passphrase)
privateKeyBytes, err := hkdf.Key(sha256.New, seed, nil, DerivationInfo, KeySize)
if err != nil {
return CryptoBoxKeypair{}, fmt.Errorf("cbk: deriving private key: %w", err)
}
publicKey, privateKey, err := box.GenerateKey(bytes.NewReader(privateKeyBytes))
if err != nil {
return CryptoBoxKeypair{}, fmt.Errorf("cbk: computing public key: %w", err)
}
return CryptoBoxKeypair{
PublicKey: CryptoBoxKey(*publicKey),
PrivateKey: CryptoBoxKey(*privateKey),
}, nil
}
// FromMnemonic derives the keypair for mnemonic with an empty passphrase.
// It is shorthand for New(Params{Mnemonic: mnemonic}).
func FromMnemonic(mnemonic string) (CryptoBoxKeypair, error) {
return New(Params{Mnemonic: mnemonic})
}
// PublicKeyBytes returns a copy of the public key as a byte slice.
func (kp CryptoBoxKeypair) PublicKeyBytes() []byte {
return kp.PublicKey.Bytes()
}
// PrivateKeyBytes returns a copy of the private key as a byte slice.
func (kp CryptoBoxKeypair) PrivateKeyBytes() []byte {
return kp.PrivateKey.Bytes()
}
// PublicKeyHex returns the public key as 64 lowercase hexadecimal
// characters.
func (kp CryptoBoxKeypair) PublicKeyHex() string {
return kp.PublicKey.Hex()
}
// PrivateKeyHex returns the private key as 64 lowercase hexadecimal
// characters.
func (kp CryptoBoxKeypair) PrivateKeyHex() string {
return kp.PrivateKey.Hex()
}
// Seal encrypts and authenticates message from this keypair to recipient.
// The result is a fresh random nonce followed by the ciphertext, so it is
// NonceSize + len(message) + box.Overhead bytes long. Only the holder of
// recipient's private key, knowing this keypair's public key, can Open it.
func (kp CryptoBoxKeypair) Seal(
message []byte, recipient CryptoBoxKey,
) ([]byte, error) {
var nonce [NonceSize]byte
_, err := rand.Read(nonce[:])
if err != nil {
return nil, fmt.Errorf("cbk: generating nonce: %w", err)
}
peersPublicKey := [KeySize]byte(recipient)
privateKey := [KeySize]byte(kp.PrivateKey)
return box.Seal(nonce[:], message, &nonce, &peersPublicKey, &privateKey), nil
}
// Open authenticates and decrypts a message that sender produced with Seal
// for this keypair. It returns ErrSealedTooShort if sealed cannot contain a
// nonce and ErrOpenFailed if authentication fails.
func (kp CryptoBoxKeypair) Open(
sealed []byte, sender CryptoBoxKey,
) ([]byte, error) {
if len(sealed) < NonceSize {
return nil, ErrSealedTooShort
}
var nonce [NonceSize]byte
copy(nonce[:], sealed[:NonceSize])
peersPublicKey := [KeySize]byte(sender)
privateKey := [KeySize]byte(kp.PrivateKey)
ciphertext := sealed[NonceSize:]
message, ok := box.Open(nil, ciphertext, &nonce, &peersPublicKey, &privateKey)
if !ok {
return nil, ErrOpenFailed
}
return message, nil
}
+203
View File
@@ -0,0 +1,203 @@
package cbk_test
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/nacl/box"
"sneak.berlin/go/x/pkg/bip39wordlist"
"sneak.berlin/go/x/pkg/cbk"
)
// vectorRepeats is how many times "abandon" appears in the standard BIP39
// test vector for all-zero 128-bit entropy, which ends with "about".
const vectorRepeats = bip39wordlist.MinWordCount - 1
// The keys below were pinned when the derivation was first implemented.
// If this test starts failing, the derivation has changed and every key
// anyone has derived with this package is no longer reproducible.
const (
vectorPublicKeyHex = "db9c1ec248805579d0b0fe2d5a4814f96009f66a11c91260487b79167f881b20"
vectorPrivateKeyHex = "eec3d5da4e348bd16df93b61c595bb5f75645f9aa468e72513be033a72b3d053"
)
// vectorMnemonic returns the standard BIP39 test vector for all-zero
// 128-bit entropy.
func vectorMnemonic() string {
return strings.Repeat("abandon ", vectorRepeats) + "about"
}
func TestNewMatchesPinnedVector(t *testing.T) {
t.Parallel()
keypair, err := cbk.FromMnemonic(vectorMnemonic())
require.NoError(t, err)
assert.Equal(t, vectorPublicKeyHex, keypair.PublicKeyHex())
assert.Equal(t, vectorPrivateKeyHex, keypair.PrivateKeyHex())
}
func TestNewNormalizesMnemonic(t *testing.T) {
t.Parallel()
canonical, err := cbk.FromMnemonic(vectorMnemonic())
require.NoError(t, err)
messy, err := cbk.New(cbk.Params{
Mnemonic: " ABANDON\tAbandon " + strings.Repeat("abandon ", 9) +
"\nabout\n",
Passphrase: "",
})
require.NoError(t, err)
assert.Equal(t, canonical, messy)
}
func TestNewPassphraseChangesKeys(t *testing.T) {
t.Parallel()
plain, err := cbk.FromMnemonic(vectorMnemonic())
require.NoError(t, err)
extraWord := "TREZOR"
withExtraWord, err := cbk.New(cbk.Params{
Mnemonic: vectorMnemonic(),
Passphrase: extraWord,
})
require.NoError(t, err)
assert.NotEqual(t, plain.PublicKey, withExtraWord.PublicKey)
assert.NotEqual(t, plain.PrivateKey, withExtraWord.PrivateKey)
again, err := cbk.New(cbk.Params{
Mnemonic: vectorMnemonic(),
Passphrase: extraWord,
})
require.NoError(t, err)
assert.Equal(t, withExtraWord, again, "derivation is deterministic")
}
func TestNewRejectsInvalidMnemonic(t *testing.T) {
t.Parallel()
_, err := cbk.FromMnemonic("abandon bogus about")
require.ErrorIs(t, err, bip39wordlist.ErrInvalidWordCount)
_, err = cbk.FromMnemonic(strings.Repeat("abandon ", vectorRepeats) + "bogus")
require.ErrorIs(t, err, bip39wordlist.ErrUnknownWord)
}
func TestKeyAccessors(t *testing.T) {
t.Parallel()
keypair, err := cbk.FromMnemonic(vectorMnemonic())
require.NoError(t, err)
assert.Equal(t, keypair.PublicKey[:], keypair.PublicKeyBytes())
assert.Equal(t, keypair.PrivateKey[:], keypair.PrivateKeyBytes())
assert.Len(t, keypair.PublicKeyBytes(), cbk.KeySize)
assert.Len(t, keypair.PrivateKeyBytes(), cbk.KeySize)
assert.Equal(t, keypair.PublicKey.Hex(), keypair.PublicKeyHex())
assert.Equal(t, keypair.PrivateKey.Hex(), keypair.PrivateKeyHex())
}
func TestKeyFromHexRoundTrip(t *testing.T) {
t.Parallel()
keypair, err := cbk.FromMnemonic(vectorMnemonic())
require.NoError(t, err)
parsed, err := cbk.KeyFromHex(keypair.PublicKeyHex())
require.NoError(t, err)
assert.Equal(t, keypair.PublicKey, parsed)
}
func TestKeyFromHexRejectsBadInput(t *testing.T) {
t.Parallel()
_, err := cbk.KeyFromHex("abcd")
require.ErrorIs(t, err, cbk.ErrInvalidKeyHex)
notHex := "zz" + keypairForTest(t).PublicKeyHex()[2:]
_, err = cbk.KeyFromHex(notHex)
require.ErrorIs(t, err, cbk.ErrInvalidKeyHex)
}
func TestSealOpenRoundTrip(t *testing.T) {
t.Parallel()
alice := keypairForTest(t)
bob := keypairForTest(t)
message := []byte("the quick brown fox jumps over the lazy dog")
sealed, err := alice.Seal(message, bob.PublicKey)
require.NoError(t, err)
assert.Len(t, sealed, cbk.NonceSize+len(message)+box.Overhead)
opened, err := bob.Open(sealed, alice.PublicKey)
require.NoError(t, err)
assert.Equal(t, message, opened)
}
func TestSealUsesFreshNonces(t *testing.T) {
t.Parallel()
alice := keypairForTest(t)
bob := keypairForTest(t)
message := []byte("same message twice")
first, err := alice.Seal(message, bob.PublicKey)
require.NoError(t, err)
second, err := alice.Seal(message, bob.PublicKey)
require.NoError(t, err)
assert.NotEqual(t, first, second)
}
func TestOpenRejectsTamperingAndWrongKeys(t *testing.T) {
t.Parallel()
alice := keypairForTest(t)
bob := keypairForTest(t)
eve := keypairForTest(t)
message := []byte("for bob only")
sealed, err := alice.Seal(message, bob.PublicKey)
require.NoError(t, err)
_, err = eve.Open(sealed, alice.PublicKey)
require.ErrorIs(t, err, cbk.ErrOpenFailed, "wrong recipient")
_, err = bob.Open(sealed, eve.PublicKey)
require.ErrorIs(t, err, cbk.ErrOpenFailed, "wrong sender")
tampered := append([]byte(nil), sealed...)
tampered[len(tampered)-1] ^= 0x01
_, err = bob.Open(tampered, alice.PublicKey)
require.ErrorIs(t, err, cbk.ErrOpenFailed, "modified ciphertext")
_, err = bob.Open(sealed[:cbk.NonceSize-1], alice.PublicKey)
require.ErrorIs(t, err, cbk.ErrSealedTooShort)
}
// keypairForTest derives a keypair from a freshly generated mnemonic.
func keypairForTest(t *testing.T) cbk.CryptoBoxKeypair {
t.Helper()
mnemonic, err := bip39wordlist.GenerateMnemonic(bip39wordlist.MaxWordCount)
require.NoError(t, err)
keypair, err := cbk.FromMnemonic(mnemonic)
require.NoError(t, err)
return keypair
}
+8
View File
@@ -0,0 +1,8 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
prettier@3.9.10:
version "3.9.10"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.9.10.tgz#ac378e432f1a7381518f2e080816bbaefa38d585"
integrity sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==