Compare commits
10
Commits
5a5577c7a5
...
f53b6aad42
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f53b6aad42 | ||
|
|
c9ce5ff442 | ||
|
|
b7028fd7e3 | ||
|
|
02386a6ff1 | ||
|
|
4d27e30ea8 | ||
|
|
157e5ef124 | ||
|
|
7be0fa7060 | ||
|
|
386155a480 | ||
|
|
3510f8a144 | ||
|
|
6f9dcc8f93 |
No files matched your search
@@ -0,0 +1,9 @@
|
||||
.git
|
||||
node_modules
|
||||
.DS_Store
|
||||
/sutil
|
||||
*.test
|
||||
*.out
|
||||
*.log
|
||||
*.prof
|
||||
coverage.*
|
||||
-10
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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.*
|
||||
@@ -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
|
||||
@@ -0,0 +1,2 @@
|
||||
node_modules/
|
||||
yarn.lock
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"tabWidth": 4,
|
||||
"proseWrap": "always"
|
||||
}
|
||||
+60
@@ -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"]
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
|
||||
[](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)
|
||||
<[sneak@sneak.berlin](mailto:sneak@sneak.berlin)>
|
||||
@@ -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`
|
||||
@@ -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
@@ -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)
|
||||
},
|
||||
}
|
||||
@@ -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
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
},
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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=
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"sneak.berlin/x/cmd"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cmd.Execute()
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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))
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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==
|
||||
Reference in new issue
Block a user