Compare commits
11
Commits
f53b6aad42
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7feb212d04 | ||
|
|
538eca9f5d | ||
|
|
09ec70009d | ||
|
|
e1b385f847 | ||
|
|
897dc9aced | ||
|
|
ad1d51a9f9 | ||
|
|
fe6dbba428 | ||
|
|
ea6faaecff | ||
|
|
5932cc12ee | ||
|
|
9def7ba586 | ||
|
|
4d45aa4ca3 |
No files matched your search
+79
-8
@@ -1,9 +1,80 @@
|
||||
.git
|
||||
node_modules
|
||||
.DS_Store
|
||||
# .dockerignore does NOT use .gitignore semantics. Docker matches with
|
||||
# moby/patternmatcher: filepath.Match plus `**`, so `*` does not cross
|
||||
# `/` and an unprefixed pattern is anchored at the context root. Every
|
||||
# depth-independent pattern therefore needs `**/`, or `config/.env` and
|
||||
# `certs/server.key` still ship while this file reads as solved. Only
|
||||
# genuinely root-anchored entries go unprefixed. Never transplant these
|
||||
# into .gitignore, where `**/` is wrong.
|
||||
#
|
||||
# Matching is case-sensitive, so secrets use character ranges rather
|
||||
# than an ALL-CAPS twin, which would still miss `Server.Key`.
|
||||
#
|
||||
# Extend with this repo's own host-built artifacts, written anchored:
|
||||
# `/myapp`, never `**/myapp`, which also matches `cmd/myapp/` and
|
||||
# deletes the package directory from the context.
|
||||
|
||||
# .git is sent without its config. Without a VERSION build argument the
|
||||
# stage that compiles runs `git describe --tags --always` on .git, which
|
||||
# does not need .git/config; that file can hold a credential, such as a
|
||||
# password in a remote URL or the token the CI checkout step stores there.
|
||||
# Each submodule keeps a config with the same exposure in its git directory
|
||||
# under .git/modules/, nested again for a submodule's own submodules, or in
|
||||
# its own .git directory when it keeps one.
|
||||
# KNOWN GAP: a submodule whose name has a `config` segment (`config`,
|
||||
# `deploy/config`, `config/lib`) loses its whole git directory, because
|
||||
# `**/.git/modules/**/config` also matches that segment's directory
|
||||
# under .git/modules/. Go's version stamping then fails the build;
|
||||
# nothing leaks. Name such a submodule without that segment:
|
||||
# `git submodule add --name`.
|
||||
**/.git/config
|
||||
**/.git/modules/**/config
|
||||
|
||||
# Agent scratch: one full checkout of the repo per in-flight agent.
|
||||
# Anchored because it occurs once where agents run at the repo root.
|
||||
# KNOWN GAP: a repo running agents in subdirectories still ships
|
||||
# `services/api/.claude/` and must add its own anchored entry.
|
||||
.claude
|
||||
|
||||
# Environment files. `*.env` covers bare `.env` and the `prod.env`
|
||||
# convention. Re-include a committed template with a negation if the
|
||||
# build needs one: `!docs/example.env`.
|
||||
**/*.[eE][nN][vV]
|
||||
**/.[eE][nN][vV].*
|
||||
**/.[eE][nN][vV][rR][cC]
|
||||
|
||||
# Private keys and the bundles carrying them. Public certificates
|
||||
# (*.crt, *.cer) are deliberately absent: they are legitimate inputs.
|
||||
**/*.[pP][eE][mM]
|
||||
**/*.[kK][eE][yY]
|
||||
**/*.[pP]12
|
||||
**/*.[pP][fF][xX]
|
||||
**/[iI][dD]_[rR][sS][aA]
|
||||
**/[iI][dD]_[dD][sS][aA]
|
||||
**/[iI][dD]_[eE][cC][dD][sS][aA]
|
||||
**/[iI][dD]_[eE][cC][dD][sS][aA]_[sS][kK]
|
||||
**/[iI][dD]_[eE][dD]25519
|
||||
**/[iI][dD]_[eE][dD]25519_[sS][kK]
|
||||
|
||||
# Dependencies: restored inside the image, never copied in.
|
||||
**/node_modules
|
||||
|
||||
# OS metadata.
|
||||
**/.DS_Store
|
||||
**/Thumbs.db
|
||||
|
||||
# Editor state: never a build input, and it churns COPY.
|
||||
**/*.swp
|
||||
**/*.swo
|
||||
**/*~
|
||||
**/*.bak
|
||||
**/.idea
|
||||
**/.vscode
|
||||
**/*.sublime-*
|
||||
|
||||
# This repository's own host-built artifacts, anchored.
|
||||
/sutil
|
||||
*.test
|
||||
*.out
|
||||
*.log
|
||||
*.prof
|
||||
coverage.*
|
||||
**/*.test
|
||||
**/*.out
|
||||
**/*.log
|
||||
**/*.prof
|
||||
**/coverage.*
|
||||
@@ -10,3 +10,9 @@ insert_final_newline = true
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
|
||||
[*.go]
|
||||
indent_style = tab
|
||||
|
||||
# This repository's own sections, such as one for another language it
|
||||
# uses, go below this comment, and a re-vendor keeps them.
|
||||
@@ -1,9 +1,20 @@
|
||||
name: check
|
||||
on: [push]
|
||||
# Free the shared runner: a new push cancels only the same branch's older run.
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
# Free the shared runner from a hung build.
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
# actions/checkout v4.2.2, 2026-02-22
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
- run: docker build .
|
||||
# script/cibuild needs no token, so none is left in .git/config.
|
||||
with:
|
||||
persist-credentials: false
|
||||
# All history and tags, so git describe finds the version tag.
|
||||
fetch-depth: 0
|
||||
- run: script/cibuild
|
||||
+39
-7
@@ -11,17 +11,49 @@ Thumbs.db
|
||||
.vscode/
|
||||
*.sublime-*
|
||||
|
||||
# Node (prettier for markdown formatting)
|
||||
# Agent scratch (worktrees of this repo, created and destroyed by
|
||||
# in-flight tooling). Unanchored: .gitignore patterns already match at
|
||||
# every depth, so no prefix is wanted here. This is not a .dockerignore
|
||||
# entry and must not be given a `**/` prefix on the way into one.
|
||||
.claude/
|
||||
|
||||
# Node
|
||||
node_modules/
|
||||
|
||||
# Environment / secrets
|
||||
.env
|
||||
.env.*
|
||||
*.pem
|
||||
*.key
|
||||
# Secrets. Unanchored like every entry above, so each matches at every
|
||||
# depth. Matching is case-sensitive on Linux, so names use character
|
||||
# ranges rather than a lowercase form that misses `Server.Key`.
|
||||
|
||||
# Go build and test artifacts
|
||||
# Environment files. `*.env` covers bare `.env` and the `prod.env`
|
||||
# convention. Only the templates `example.env` and `sample.env` are
|
||||
# re-included below. A repository that commits any other template adds
|
||||
# its own negation at the end of this file, for example `!.env.example`.
|
||||
*.[eE][nN][vV]
|
||||
.[eE][nN][vV].*
|
||||
.[eE][nN][vV][rR][cC]
|
||||
!example.env
|
||||
!sample.env
|
||||
|
||||
# Private keys and the bundles carrying them.
|
||||
*.[pP][eE][mM]
|
||||
*.[kK][eE][yY]
|
||||
*.[pP]12
|
||||
*.[pP][fF][xX]
|
||||
[iI][dD]_[rR][sS][aA]
|
||||
[iI][dD]_[dD][sS][aA]
|
||||
[iI][dD]_[eE][cC][dD][sS][aA]
|
||||
[iI][dD]_[eE][cC][dD][sS][aA]_[sS][kK]
|
||||
[iI][dD]_[eE][dD]25519
|
||||
[iI][dD]_[eE][dD]25519_[sS][kK]
|
||||
|
||||
# This repository's own entries, such as its build outputs, go below
|
||||
# this comment, and a re-vendor keeps them. Anchor a binary built at the
|
||||
# root: `/myapp`, never `myapp`, which also ignores `cmd/myapp/`.
|
||||
|
||||
# The sutil binary that `make build` writes at the root.
|
||||
/sutil
|
||||
|
||||
# Go test and profiling outputs.
|
||||
*.test
|
||||
*.out
|
||||
*.log
|
||||
|
||||
+105
-15
@@ -1,32 +1,122 @@
|
||||
version: "2"
|
||||
|
||||
# Config schema uses the golangci-lint v2 layout (settings live under
|
||||
# linters.settings, not top-level linters-settings) so that the
|
||||
# thresholds below are actually applied by golangci-lint >= v2.
|
||||
#
|
||||
# This file has no formatters section, so golangci-lint run does not run
|
||||
# gofmt. A repository's script/fmt-check runs it, on the host.
|
||||
|
||||
run:
|
||||
timeout: 5m
|
||||
modules-download-mode: readonly
|
||||
|
||||
linters:
|
||||
default: all
|
||||
enable:
|
||||
# Successor to the deprecated gomodguard. Named explicitly, rather than
|
||||
# left to `default: all`, because it carries the module policy below.
|
||||
- gomodguard_v2
|
||||
disable:
|
||||
# Genuinely incompatible with project patterns
|
||||
- exhaustruct # Requires all struct fields
|
||||
- depguard # Dependency allow/block lists
|
||||
- exhaustruct_v5 # Requires all struct fields
|
||||
- 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
|
||||
# Deprecated: the warning is attached to the old name, so it is
|
||||
# silenced by disabling that name, not by enabling the successor.
|
||||
- exhaustruct # Deprecated, replaced by exhaustruct_v5
|
||||
- wsl # Deprecated, replaced by wsl_v5
|
||||
- gomodguard # Deprecated, replaced by gomodguard_v2
|
||||
settings:
|
||||
lll:
|
||||
line-length: 88
|
||||
funlen:
|
||||
lines: 80
|
||||
statements: 50
|
||||
cyclop:
|
||||
max-complexity: 15
|
||||
dupl:
|
||||
threshold: 100
|
||||
depguard:
|
||||
# Test-support code must not be compiled into the shipped binary. A
|
||||
# test-support package exists to hand a test privileges the program
|
||||
# itself must never have, so a file that is not a test must not import
|
||||
# one. Test files, and the files inside a package whose directory name
|
||||
# ends in `test`, are where that code belongs, and are exempt. A package
|
||||
# in a subdirectory of that directory is not exempt. `${base-path}` is
|
||||
# the directory holding this file, so the name of the directory a
|
||||
# repository is cloned into does not count.
|
||||
#
|
||||
# The deny list below is the one part of this file a repository is
|
||||
# expected to extend, and the only part it may. depguard matches an
|
||||
# import path against a list of prefixes, so it cannot be told "any path
|
||||
# whose last segment ends in test"; a repository's own test-support
|
||||
# packages have to be named here one at a time, by full import path,
|
||||
# under a module path that differs from repository to repository. Add
|
||||
# them; change nothing else.
|
||||
rules:
|
||||
test-support:
|
||||
list-mode: lax
|
||||
files:
|
||||
- "$all"
|
||||
- "!$test"
|
||||
- "!${base-path}**/*test/*"
|
||||
deny:
|
||||
- pkg: net/http/httptest
|
||||
desc: >-
|
||||
Test-support code belongs in test files and in packages whose
|
||||
directory name ends in test, not in the shipped binary.
|
||||
# Each entry names a module not to use and what to use instead. Every
|
||||
# entry matches the module path exactly.
|
||||
gomodguard_v2:
|
||||
blocked:
|
||||
- module: github.com/rs/zerolog
|
||||
recommendations:
|
||||
- log/slog
|
||||
reason: "Structured logging is stdlib log/slog."
|
||||
# These entries cover only the github.com/go-redis/redis module paths,
|
||||
# one entry per path, because the later releases are separate paths.
|
||||
# A prefix match would be shorter but would also reach
|
||||
# github.com/go-redis/redismock/v9, the test double for the successor
|
||||
# these entries recommend.
|
||||
- module: github.com/go-redis/redis
|
||||
recommendations:
|
||||
- github.com/redis/go-redis/v9
|
||||
reason: "Pre-fork module; use the maintained go-redis v9."
|
||||
- module: github.com/go-redis/redis/v7
|
||||
recommendations:
|
||||
- github.com/redis/go-redis/v9
|
||||
reason: "Pre-fork module; use the maintained go-redis v9."
|
||||
- module: github.com/go-redis/redis/v8
|
||||
recommendations:
|
||||
- github.com/redis/go-redis/v9
|
||||
reason: "Pre-fork module; use the maintained go-redis v9."
|
||||
- module: github.com/go-redis/redis/v9
|
||||
recommendations:
|
||||
- github.com/redis/go-redis/v9
|
||||
reason: "Pre-fork module; use the maintained go-redis v9."
|
||||
- module: github.com/sergi/go-diff
|
||||
recommendations:
|
||||
- github.com/aymanbagabas/go-udiff
|
||||
reason: "No unified diff output; use go-udiff."
|
||||
- module: github.com/hexops/gotextdiff
|
||||
recommendations:
|
||||
- github.com/aymanbagabas/go-udiff
|
||||
reason: "Unmaintained fork; use go-udiff."
|
||||
exclusions:
|
||||
rules:
|
||||
# homoicon keeps a local copy of sobek, its JavaScript engine, as sneak
|
||||
# decided in
|
||||
# https://git.eeqj.de/sneak/homoicon/issues/1759#issuecomment-153261.
|
||||
# This is an exclusion because gomoddirectives does not check its
|
||||
# replace-allow-list for a replace that points at a local path.
|
||||
- path: ^go\.mod$
|
||||
linters:
|
||||
- gomoddirectives
|
||||
text: '^local replacement are not allowed: github\.com/grafana/sobek$'
|
||||
|
||||
issues:
|
||||
exclude-use-default: false
|
||||
max-issues-per-linter: 0
|
||||
max-same-issues: 0
|
||||
uniq-by-line: false
|
||||
@@ -0,0 +1,19 @@
|
||||
# AGENTS.md
|
||||
|
||||
Guidance for coding agents working in this repository. The repository policies
|
||||
are in `REPO_POLICIES.md`, vendored from the `prompts` repository; read that
|
||||
first. Everything below is specific to this repository, and the README's Design
|
||||
section explains each point.
|
||||
|
||||
- `pkg/` holds only packages designed to be imported by other programs. Anything
|
||||
that exists to support `sutil` lives under `internal/`.
|
||||
- `sutil` is not a command-line front end for the packages in `pkg/`. It holds
|
||||
only the utilities the author wants available everywhere; a command is added
|
||||
because a tool is wanted, not because a package exists.
|
||||
- `pkg/cbk` derives crypto_box keypairs from BIP39 mnemonics with BIP85 and does
|
||||
nothing else. BIP39 passphrases are deliberately unsupported.
|
||||
- cgo stays off: builds are static, and the test phase runs without `-race`.
|
||||
- Version tags are created by the author alone, when the author decides. Do not
|
||||
propose, schedule, or create tags.
|
||||
- `.golangci.yml` is byte-identical to the canonical copy and is not edited
|
||||
here; changes go to the `prompts` repository.
|
||||
+72
-37
@@ -1,60 +1,95 @@
|
||||
# The checks behind `make check` are split across independent stages so that
|
||||
# formatting and lint failures surface in seconds, before any compilation:
|
||||
# In every stage ARG CACHEBUST comes right after the steps that install
|
||||
# system packages and download Go's modules, or right after FROM in a
|
||||
# stage with neither, and every step that lints, tests or compiles
|
||||
# comes after it. script/ passes a new value on every build, which
|
||||
# reruns every RUN step after it. Docker keeps the layers above it
|
||||
# until the base image, their own lines or go.mod and go.sum change.
|
||||
# The steps that compile mount Go's build cache, and golangci-lint run
|
||||
# also mounts golangci-lint's; both survive from build to build.
|
||||
#
|
||||
# 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.
|
||||
# This repository keeps cgo off, so its test phase departs from the
|
||||
# canonical Go Dockerfile in REPO_POLICIES.md in one way: it runs on the
|
||||
# alpine Go image without -race, which needs a C toolchain. Everything
|
||||
# else follows the canonical file.
|
||||
|
||||
# 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
|
||||
# Lint phase
|
||||
# golangci/golangci-lint:v2.15.0, 2026-10-16
|
||||
FROM golangci/golangci-lint@sha256:fbfcfd12a6f8d7f1b97b3362ca80fc2a244d46863148c73cb88e7793af96c481 AS lint
|
||||
WORKDIR /src
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
ARG CACHEBUST
|
||||
COPY . .
|
||||
RUN make fmt-check-go
|
||||
RUN make lint
|
||||
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||
--mount=type=cache,target=/root/.cache/golangci-lint \
|
||||
golangci-lint run --config .golangci.yml ./...
|
||||
|
||||
# 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
|
||||
# Test phase. -count=1 because the mounted cache also holds test
|
||||
# results, which go test would otherwise replay. A first run that timed
|
||||
# out gets no rerun, because a rerun would only wait for the same hang.
|
||||
# golang:1.26-alpine, 2026-10-27
|
||||
FROM golang@sha256:a2012b4a1d1f9f7e9f44e0d39b805a901e856b1dd80b4e6ee4338db547050308 AS test
|
||||
ENV CGO_ENABLED=0
|
||||
WORKDIR /src
|
||||
COPY package.json yarn.lock ./
|
||||
RUN yarn install --frozen-lockfile
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
ARG CACHEBUST
|
||||
COPY . .
|
||||
RUN make fmt-check-md
|
||||
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||
status=0; \
|
||||
output=$(go test -count=1 -timeout 90s -cover ./... 2>&1) || status=$?; \
|
||||
printf '%s\n' "$output"; \
|
||||
test "$status" -eq 0 && exit 0; \
|
||||
case "$output" in \
|
||||
*"panic: test timed out"*|*"*** Test killed"*) exit 1 ;; \
|
||||
esac; \
|
||||
echo "--- Rerunning with -v for details ---"; \
|
||||
go test -count=1 -timeout 90s -v ./...; \
|
||||
exit 1
|
||||
|
||||
# Build stage
|
||||
# Build stage. Nothing is wanted from either phase above; the copies
|
||||
# are what make BuildKit build them first, so nothing after them can
|
||||
# run unless lint and test passed.
|
||||
# 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
|
||||
# git comes from the base image's own distribution package index, which
|
||||
# is trusted, so it needs no version and no hash.
|
||||
RUN apk add --no-cache git
|
||||
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
|
||||
ARG CACHEBUST
|
||||
COPY --from=lint /src/go.sum /dev/null
|
||||
COPY --from=test /src/go.sum /dev/null
|
||||
# A tar-stream context keeps the sender's file owners, which git refuses.
|
||||
RUN git config --system --add safe.directory /src
|
||||
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
|
||||
# The VERSION build arg when one is given, otherwise
|
||||
# `git describe --tags --always` on the .git in the build context. With
|
||||
# .git present, a version that is still empty, dev or unknown fails the
|
||||
# build: git is missing or could not read the checkout.
|
||||
ARG VERSION
|
||||
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||
VERSION="${VERSION:-$(git describe --tags --always)}"; \
|
||||
if [ -e .git ]; then \
|
||||
case "$VERSION" in ""|dev|unknown) \
|
||||
echo "version is '$VERSION' although .git is present" >&2; \
|
||||
exit 1 ;; \
|
||||
esac; \
|
||||
fi; \
|
||||
CGO_ENABLED=0 go build -trimpath \
|
||||
-ldflags="-s -w -X main.Version=${VERSION}" \
|
||||
-o /sutil ./cmd/sutil
|
||||
# The runtime stage ships the source; the repository history is not
|
||||
# part of it.
|
||||
RUN rm -rf /src/.git
|
||||
|
||||
# Runtime stage: the binary plus the full source it was built from
|
||||
# Runtime stage, and the last one: the binary plus the source it was
|
||||
# built from.
|
||||
# alpine:3.24, 2026-10-23
|
||||
FROM alpine@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a
|
||||
ARG CACHEBUST
|
||||
COPY --from=builder /sutil /usr/local/bin/sutil
|
||||
COPY --from=builder /src /usr/local/src/x
|
||||
ENTRYPOINT ["sutil"]
|
||||
@@ -1,29 +1,46 @@
|
||||
# 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: bootstrap setup test lint fmt fmt-check check docker hooks \
|
||||
build install tidy clean
|
||||
|
||||
.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
|
||||
# Makefile targets are thin shims; the implementations live in script/
|
||||
# per the scripts-to-rule-them-all pattern (see the Entrypoints section
|
||||
# of README.md). The targets after the shims are this repo's own.
|
||||
|
||||
bootstrap:
|
||||
@script/bootstrap
|
||||
|
||||
setup:
|
||||
@script/setup
|
||||
|
||||
test:
|
||||
@script/test
|
||||
|
||||
lint:
|
||||
@script/lint
|
||||
|
||||
fmt:
|
||||
@script/fmt
|
||||
|
||||
fmt-check:
|
||||
@script/fmt-check
|
||||
|
||||
check:
|
||||
@script/check
|
||||
|
||||
docker:
|
||||
@script/docker
|
||||
|
||||
hooks:
|
||||
@script/install-precommit
|
||||
|
||||
# This repository's own targets.
|
||||
|
||||
BINARY := sutil
|
||||
VERSION := $(shell git describe --always --dirty 2>/dev/null || echo dev)
|
||||
GOLDFLAGS := -X main.version=$(VERSION)
|
||||
GO_DIRS := cmd internal pkg
|
||||
VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo unknown)
|
||||
GOLDFLAGS := -X main.Version=$(VERSION)
|
||||
|
||||
# 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
|
||||
# cgo stays off everywhere: builds are static, and the test phase of the
|
||||
# Dockerfile runs without -race, which would need a C toolchain.
|
||||
export CGO_ENABLED := 0
|
||||
|
||||
# Build the sutil binary in the repo root with the git version embedded.
|
||||
build:
|
||||
@@ -33,76 +50,9 @@ build:
|
||||
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 and go.sum; script/precommit refuses a commit that needs it.
|
||||
tidy:
|
||||
go mod tidy
|
||||
|
||||
# One-time developer setup: install the pinned prettier for markdown checks.
|
||||
setup:
|
||||
yarn install --frozen-lockfile
|
||||
|
||||
clean:
|
||||
rm -f $(BINARY)
|
||||
@@ -4,8 +4,8 @@
|
||||
|
||||
`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.
|
||||
small reusable packages, plus `sutil`, a command-line utility holding a few
|
||||
small tools worth having on every machine.
|
||||
|
||||
Importable packages live in `pkg/`. The CLI is `cmd/sutil/`, and the code that
|
||||
exists only to support it lives in `internal/`.
|
||||
@@ -39,18 +39,13 @@ sutil generate random --count 1073741824 "my seed" > testdata.bin
|
||||
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.
|
||||
# Build metadata, including which config file (if any) was loaded.
|
||||
sutil version
|
||||
sutil info
|
||||
|
||||
# Every command first loads ~/.sneak.yaml (or .json, .toml, ...) if it
|
||||
# exists, or the file named with --config.
|
||||
sutil --config ./sneak.yaml info
|
||||
```
|
||||
|
||||
### Use the packages
|
||||
@@ -71,30 +66,15 @@ func main() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// The same mnemonic (and optional passphrase) always yields the same
|
||||
// keypair, on any machine.
|
||||
alice, err := cbk.New(cbk.Params{Mnemonic: mnemonic})
|
||||
// The same mnemonic always yields the same keypair, on any machine.
|
||||
// The keys are plain crypto_box keys for use with
|
||||
// golang.org/x/crypto/nacl/box.
|
||||
keypair, 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())
|
||||
fmt.Println(keypair.PublicKeyHex())
|
||||
}
|
||||
```
|
||||
|
||||
@@ -138,37 +118,46 @@ 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.
|
||||
output, and nothing happens in `init` functions. Before any command runs, the
|
||||
root command's persistent pre-run loads the optional configuration file (the one
|
||||
named by `--config`, or else `.sneak.*` in the home directory found with
|
||||
`go-homedir`) into a viper instance that also exposes environment variables. No
|
||||
command consumes settings from it yet; `sutil info` reports which file was
|
||||
loaded. Tests point `Params.ConfigDir` at a temporary directory so the real home
|
||||
directory is never read.
|
||||
|
||||
### `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:
|
||||
Derives NaCl crypto_box keypairs (Curve25519) deterministically from BIP39
|
||||
mnemonics, and nothing else. The entropy comes from
|
||||
[BIP85](https://github.com/bitcoin/bips/blob/master/bip-0085.mediawiki) through
|
||||
`sneak.berlin/go/secret/pkg/bip85`, so any BIP85-capable tool given the same
|
||||
mnemonic reproduces the same key, and one mnemonic can back any number of
|
||||
independent keypairs:
|
||||
|
||||
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.
|
||||
rounds, salt `"mnemonic"`), exactly as a BIP39 wallet would with an empty
|
||||
passphrase. BIP39 passphrases are deliberately not supported.
|
||||
3. The seed becomes a BIP32 master key.
|
||||
4. BIP85's HEX application derives 32 bytes of entropy at
|
||||
`m/83696968'/128169'/32'/{index}'`; those bytes are the private key.
|
||||
5. 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:
|
||||
The resulting keys are plain 32-byte crypto_box keys for use with
|
||||
`golang.org/x/crypto/nacl/box`. 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.
|
||||
- `cbk.New(cbk.Params{Mnemonic, Index})` and `cbk.FromMnemonic(m)` (index 0)
|
||||
return a `CryptoBoxKeypair`.
|
||||
- `CryptoBoxKey.Hex()`, `CryptoBoxKey.Bytes()`, 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.
|
||||
The test suite checks the mnemonic-to-master-key step and the BIP85 HEX
|
||||
application against the BIP85 specification's own test vectors, verifies that
|
||||
the private key is exactly the BIP85 output, and pins the keypair for the
|
||||
specification's mnemonic at index 0. If the pinned test ever fails, the
|
||||
derivation has changed and existing keys are no longer reproducible.
|
||||
|
||||
### `pkg/bip39wordlist`
|
||||
|
||||
@@ -218,53 +207,89 @@ sutil
|
||||
├── 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)
|
||||
│ └── password (pw) print a memorable password
|
||||
```
|
||||
|
||||
`sutil` is not a command-line front end for the packages in `pkg/`. It holds
|
||||
only the handful of utilities the author wants available everywhere. `pkg/cbk`
|
||||
and `pkg/bip39wordlist` are libraries with no CLI surface, and a command is
|
||||
added because a tool is wanted, not because a package exists.
|
||||
|
||||
### 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.
|
||||
The gates live in the `Dockerfile`, in the canonical Go layout that
|
||||
`REPO_POLICIES.md` prescribes: a `lint` phase on the hash-pinned golangci-lint
|
||||
image and a `test` phase on the Go image. `script/lint` and `script/test` each
|
||||
build one phase alone, with a fresh `CACHEBUST` value so the check step runs on
|
||||
every build instead of being served from Docker's cache, and write no image. The
|
||||
build stage depends on both phases, so the image cannot be built unless they
|
||||
pass. Go's build cache and golangci-lint's cache are mounted into the phases and
|
||||
survive between builds. Formatting is the one check that runs on the host:
|
||||
`script/fmt-check` runs gofmt and the prettier pinned in `yarn.lock`.
|
||||
`script/check` runs all three. `script/cibuild`, which the Gitea workflow calls,
|
||||
bootstraps, runs `script/check`, and then builds the tagged image with the
|
||||
version from `git describe`.
|
||||
|
||||
`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.
|
||||
comment. `.golangci.yml` is byte-identical to the canonical copy in the
|
||||
`prompts` repository and is never edited here; the lint phase pins the
|
||||
golangci-lint release that the policy names. cgo stays off: the Makefile exports
|
||||
`CGO_ENABLED=0`, binaries are static, and the test phase runs on the alpine Go
|
||||
image without `-race`, which is this repository's one departure from the
|
||||
canonical `Dockerfile`. The runtime image carries the binary and the source it
|
||||
was built from.
|
||||
|
||||
Version tags are created by the author alone, when the author decides. Tooling,
|
||||
contributors, and agents working in this repository do not propose, schedule, or
|
||||
create tags.
|
||||
|
||||
## Entrypoints
|
||||
|
||||
This repository adheres to the
|
||||
[Scripts to Rule Them All](https://github.com/github/scripts-to-rule-them-all)
|
||||
standard: normalized scripts in `script/` are the entrypoints for the
|
||||
development workflow, and the Makefile targets are thin shims that call them. We
|
||||
provide:
|
||||
|
||||
- `script/bootstrap`: install everything the repository needs, idempotently:
|
||||
make, git, go, and, because `yarn.lock` is present, node, yarn 1 and the
|
||||
pinned prettier.
|
||||
- `script/setup`: make a fresh clone ready for development by running
|
||||
`script/bootstrap` and then `script/install-precommit`.
|
||||
- `script/projectname`: print the project name, `x`, which `script/docker` and
|
||||
`script/cibuild` use as the image tag.
|
||||
- `script/test`: build the `test` phase of the `Dockerfile` with a new
|
||||
`CACHEBUST` value, writing no image.
|
||||
- `script/lint`: build the `lint` phase the same way.
|
||||
- `script/fmt`: gofmt and prettier, writing files.
|
||||
- `script/fmt-check`: the read-only twin of `script/fmt`.
|
||||
- `script/check`: `script/test`, `script/lint`, then `script/fmt-check`. It
|
||||
modifies nothing.
|
||||
- `script/docker`: build the image tagged `x`, passing the version from
|
||||
`git describe`.
|
||||
- `script/cibuild`: what CI runs: `script/bootstrap`, `script/check`, then the
|
||||
tagged image build.
|
||||
- `script/precommit`: run by the pre-commit hook. It refuses a commit whose
|
||||
`go.mod` or `go.sum` is not tidy, then runs `script/check`.
|
||||
- `script/install-precommit`: install that hook; `make hooks` shims to it.
|
||||
|
||||
The Makefile adds four targets of its own: `make build`, `make install`,
|
||||
`make tidy`, and `make clean`.
|
||||
|
||||
## Development
|
||||
|
||||
```sh
|
||||
make setup # once: install the pinned prettier with yarn
|
||||
make check # test + lint + fmt-check, exactly what CI gates on
|
||||
make setup # once: bootstrap and install the pre-commit hook
|
||||
make check # test, lint, fmt-check: what the hook and CI run
|
||||
make fmt # gofmt and prettier, writes files
|
||||
make hooks # install the pre-commit hook that runs make check
|
||||
make docker # the full CI build
|
||||
make build # build ./sutil on the host
|
||||
make docker # build the image tagged x
|
||||
```
|
||||
|
||||
## 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.
|
||||
Nothing outstanding.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
+877
-138
File diff suppressed because it is too large.
Load diff
+8
-6
@@ -1,5 +1,5 @@
|
||||
// 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.
|
||||
// Command sutil is sneak's shell utility: a few small tools worth having on
|
||||
// every machine. Run `sutil --help` for the command list.
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -8,12 +8,14 @@ import (
|
||||
"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"
|
||||
// Version is the build version. The Makefile and Dockerfile set it at link
|
||||
// time with -ldflags "-X main.Version=...".
|
||||
//
|
||||
//nolint:gochecknoglobals // set by the linker; there is no other way
|
||||
var Version = "dev"
|
||||
|
||||
func main() {
|
||||
err := cli.New(cli.Params{Version: version}).Execute()
|
||||
err := cli.New(cli.Params{Version: Version, ConfigDir: ""}).Execute()
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@@ -3,16 +3,32 @@ module sneak.berlin/go/x
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
github.com/btcsuite/btcd v0.24.2
|
||||
github.com/btcsuite/btcd/btcutil v1.1.6
|
||||
github.com/kavfixnel/words v1.0.0
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/spf13/viper v1.21.0
|
||||
github.com/stretchr/testify v1.12.1
|
||||
github.com/tyler-smith/go-bip39 v1.1.0
|
||||
golang.org/x/crypto v0.58.0
|
||||
sneak.berlin/go/secret v1.0.0-rc4
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.1.3 // indirect
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.9 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // 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,30 +1,166 @@
|
||||
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
|
||||
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
|
||||
github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M=
|
||||
github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A=
|
||||
github.com/btcsuite/btcd v0.24.2 h1:aLmxPguqxza+4ag8R1I2nnJjSu2iFn/kqtHTIImswcY=
|
||||
github.com/btcsuite/btcd v0.24.2/go.mod h1:5C8ChTkl5ejr3WHj8tkQSCmydiMEPB0ZhQhehpq7Dgg=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.1.3 h1:xM/n3yIhHAhHy04z4i43C8p4ehixJZMsnrVJkgl+MTE=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE=
|
||||
github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A=
|
||||
github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE=
|
||||
github.com/btcsuite/btcd/btcutil v1.1.5/go.mod h1:PSZZ4UitpLBWzxGd5VGOrLnmOjtPP/a6HaFo12zMs00=
|
||||
github.com/btcsuite/btcd/btcutil v1.1.6 h1:zFL2+c3Lb9gEgqKNzowKUPQNb8jV7v5Oaodi/AYFd6c=
|
||||
github.com/btcsuite/btcd/btcutil v1.1.6/go.mod h1:9dFymx8HpuLqBnsPELrImQeTQfKBQqzqGbbV3jK55aE=
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ=
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
||||
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
|
||||
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
|
||||
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
|
||||
github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
|
||||
github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I=
|
||||
github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
|
||||
github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
|
||||
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
|
||||
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
|
||||
github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
|
||||
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/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
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/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
|
||||
github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
|
||||
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||
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/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
||||
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
|
||||
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
|
||||
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-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.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-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/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.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
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=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
sneak.berlin/go/secret v1.0.0-rc4 h1:BFpPs34qhcYS2ZtK+nvWmBT7CGFgjYWY9KA9Amske3w=
|
||||
sneak.berlin/go/secret v1.0.0-rc4/go.mod h1:pvhNbatHqGCO9l7hXYjlKEHkpGh7u8q/uE7mzP9EZXo=
|
||||
@@ -1,82 +0,0 @@
|
||||
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
|
||||
}
|
||||
+31
-7
@@ -7,6 +7,7 @@ package cli
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// Params configures New.
|
||||
@@ -14,25 +15,48 @@ 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
|
||||
|
||||
// ConfigDir is the directory searched for the optional .sneak
|
||||
// configuration file. Empty means the user's home directory; tests set
|
||||
// it to a temporary directory so the real home is never consulted.
|
||||
ConfigDir string
|
||||
}
|
||||
|
||||
// app carries the state shared by every command: the build parameters,
|
||||
// the --config flag, and the configuration loaded before any command runs.
|
||||
type app struct {
|
||||
params Params
|
||||
configFile string
|
||||
config *viper.Viper
|
||||
}
|
||||
|
||||
// New builds the root sutil command with every subcommand attached.
|
||||
func New(p Params) *cobra.Command {
|
||||
a := &app{params: p, configFile: "", config: nil}
|
||||
|
||||
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,
|
||||
Long: `sutil is a small collection of shell utilities worth having on every
|
||||
machine: a fast pseudorandom byte stream and a memorable password generator.
|
||||
|
||||
Before any command runs, sutil loads the optional configuration file named
|
||||
by --config, or else ~/.sneak.yaml (or .json, .toml, ...) if it exists, and
|
||||
makes environment variables available alongside it.`,
|
||||
Version: p.Version,
|
||||
SilenceUsage: true,
|
||||
PersistentPreRunE: a.loadConfig,
|
||||
}
|
||||
|
||||
root.PersistentFlags().StringVar(
|
||||
&a.configFile, "config", "",
|
||||
"config file (default is "+configName+".* in the home directory)",
|
||||
)
|
||||
|
||||
root.AddCommand(
|
||||
newVersionCommand(p),
|
||||
newInfoCommand(p),
|
||||
newInfoCommand(a),
|
||||
newGenerateCommand(),
|
||||
newCBKCommand(),
|
||||
)
|
||||
|
||||
return root
|
||||
|
||||
+82
-73
@@ -3,6 +3,8 @@ package cli_test
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -11,29 +13,14 @@ import (
|
||||
|
||||
"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"
|
||||
}
|
||||
const testVersion = "v1.2.3-test"
|
||||
|
||||
func TestVersionCommand(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
output, err := execute(t, "", "version")
|
||||
output, err := execute(t, "version")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, testVersion+"\n", output)
|
||||
}
|
||||
@@ -41,7 +28,7 @@ func TestVersionCommand(t *testing.T) {
|
||||
func TestVersionFlag(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
output, err := execute(t, "", "--version")
|
||||
output, err := execute(t, "--version")
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, output, testVersion)
|
||||
}
|
||||
@@ -49,25 +36,75 @@ func TestVersionFlag(t *testing.T) {
|
||||
func TestInfoCommand(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
output, err := execute(t, "", "info")
|
||||
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")
|
||||
assert.Contains(t, output, "config: (none)\n")
|
||||
}
|
||||
|
||||
func TestConfigFileDiscoveredInConfigDir(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
configDir := t.TempDir()
|
||||
path := filepath.Join(configDir, ".sneak.yaml")
|
||||
require.NoError(t, os.WriteFile(path, []byte("example: 1\n"), 0o600))
|
||||
|
||||
params := cli.Params{Version: testVersion, ConfigDir: configDir}
|
||||
|
||||
output, err := executeWithParams(t, params, "info")
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, output, "Using config file: "+path+"\n")
|
||||
assert.Contains(t, output, "config: "+path+"\n")
|
||||
}
|
||||
|
||||
func TestConfigFlagLoadsNamedFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
path := filepath.Join(t.TempDir(), "elsewhere.yaml")
|
||||
require.NoError(t, os.WriteFile(path, []byte("example: 1\n"), 0o600))
|
||||
|
||||
output, err := execute(t, "--config", path, "info")
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, output, "Using config file: "+path+"\n")
|
||||
assert.Contains(t, output, "config: "+path+"\n")
|
||||
}
|
||||
|
||||
func TestConfigFlagRejectsMissingFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
path := filepath.Join(t.TempDir(), "missing.yaml")
|
||||
|
||||
_, err := execute(t, "--config", path, "version")
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestConfigRejectsMalformedFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
configDir := t.TempDir()
|
||||
path := filepath.Join(configDir, ".sneak.yaml")
|
||||
require.NoError(t, os.WriteFile(path, []byte("not: [valid\n"), 0o600))
|
||||
|
||||
params := cli.Params{Version: testVersion, ConfigDir: configDir}
|
||||
|
||||
_, err := executeWithParams(t, params, "version")
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestGenerateRandomSeededIsDeterministic(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
first, err := execute(t, "", "generate", "random", "--count", "4096", "seed")
|
||||
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")
|
||||
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")
|
||||
other, err := execute(t, "gen", "random", "-c", "4096", "other seed")
|
||||
require.NoError(t, err)
|
||||
assert.NotEqual(t, first, other)
|
||||
}
|
||||
@@ -75,11 +112,11 @@ func TestGenerateRandomSeededIsDeterministic(t *testing.T) {
|
||||
func TestGenerateRandomUnseeded(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
first, err := execute(t, "", "generate", "random", "--count", "1024")
|
||||
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")
|
||||
second, err := execute(t, "generate", "random", "--count", "1024")
|
||||
require.NoError(t, err)
|
||||
assert.NotEqual(t, first, second)
|
||||
}
|
||||
@@ -87,28 +124,10 @@ func TestGenerateRandomUnseeded(t *testing.T) {
|
||||
func TestGenerateRandomRejectsExtraArguments(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := execute(t, "", "generate", "random", "one", "two")
|
||||
_, 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()
|
||||
|
||||
@@ -119,7 +138,7 @@ func TestGeneratePassword(t *testing.T) {
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
output, err := execute(t, "", "generate", "password")
|
||||
output, err := execute(t, "generate", "password")
|
||||
require.NoError(t, err)
|
||||
|
||||
password := strings.TrimSuffix(output, "\n")
|
||||
@@ -129,46 +148,36 @@ func TestGeneratePassword(t *testing.T) {
|
||||
assert.True(t, strings.ContainsAny(password, pwgen.Delimiters), password)
|
||||
}
|
||||
|
||||
func TestCBKDerive(t *testing.T) {
|
||||
func TestUnknownCommandFails(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)
|
||||
_, err := execute(t, "cbk")
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
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) {
|
||||
// execute runs sutil with the given arguments, looking for config in an
|
||||
// empty temporary directory, and returns everything it wrote to stdout and
|
||||
// stderr.
|
||||
func execute(t *testing.T, args ...string) (string, error) {
|
||||
t.Helper()
|
||||
|
||||
root := cli.New(cli.Params{Version: testVersion})
|
||||
params := cli.Params{Version: testVersion, ConfigDir: t.TempDir()}
|
||||
|
||||
return executeWithParams(t, params, args...)
|
||||
}
|
||||
|
||||
// executeWithParams is execute with explicit Params.
|
||||
func executeWithParams(
|
||||
t *testing.T, params cli.Params, args ...string,
|
||||
) (string, error) {
|
||||
t.Helper()
|
||||
|
||||
root := cli.New(params)
|
||||
|
||||
var output bytes.Buffer
|
||||
|
||||
root.SetOut(&output)
|
||||
root.SetErr(&output)
|
||||
root.SetIn(strings.NewReader(stdin))
|
||||
root.SetArgs(args)
|
||||
|
||||
err := root.Execute()
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// configName is the base name of the optional configuration file looked
|
||||
// up in the config directory; viper tries every extension it supports, so
|
||||
// ~/.sneak.yaml, ~/.sneak.json, ~/.sneak.toml, and so on all work.
|
||||
const configName = ".sneak"
|
||||
|
||||
// loadConfig runs before every command. It reads the optional
|
||||
// configuration file into a.config and makes environment variables
|
||||
// available through it. The file is the one named by --config, or else
|
||||
// configName in Params.ConfigDir, which defaults to the user's home
|
||||
// directory. A missing default file is not an error; a file that is named
|
||||
// explicitly but missing, or one that cannot be parsed, is.
|
||||
func (a *app) loadConfig(cmd *cobra.Command, _ []string) error {
|
||||
configDir := a.params.ConfigDir
|
||||
if configDir == "" {
|
||||
home, err := homedir.Dir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("finding home directory: %w", err)
|
||||
}
|
||||
|
||||
configDir = home
|
||||
}
|
||||
|
||||
config := viper.New()
|
||||
config.AddConfigPath(configDir)
|
||||
config.SetConfigName(configName)
|
||||
config.AutomaticEnv()
|
||||
|
||||
if a.configFile != "" {
|
||||
config.SetConfigFile(a.configFile)
|
||||
}
|
||||
|
||||
err := config.ReadInConfig()
|
||||
|
||||
_, isNotFound := errors.AsType[viper.ConfigFileNotFoundError](err)
|
||||
if isNotFound {
|
||||
a.config = config
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading config file: %w", err)
|
||||
}
|
||||
|
||||
a.config = config
|
||||
|
||||
_, err = fmt.Fprintln(
|
||||
cmd.ErrOrStderr(), "Using config file:", config.ConfigFileUsed(),
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// configFileUsed returns the path of the configuration file that
|
||||
// loadConfig read, or an empty string if there was none.
|
||||
func (a *app) configFileUsed() string {
|
||||
if a.config == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return a.config.ConfigFileUsed()
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
|
||||
"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
|
||||
@@ -21,13 +20,12 @@ func newGenerateCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "generate",
|
||||
Aliases: []string{"gen"},
|
||||
Short: "Generate random bytes, passwords, and mnemonics",
|
||||
Short: "Generate random bytes and passwords",
|
||||
}
|
||||
|
||||
cmd.AddCommand(
|
||||
newGenerateRandomCommand(),
|
||||
newGeneratePasswordCommand(),
|
||||
newGenerateMnemonicCommand(),
|
||||
)
|
||||
|
||||
return cmd
|
||||
@@ -107,36 +105,6 @@ no usable word list.`,
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
|
||||
+15
-5
@@ -15,31 +15,41 @@ import (
|
||||
// module support.
|
||||
var ErrNoBuildInfo = errors.New("sutil: no build information in this binary")
|
||||
|
||||
// noConfigFile is what `sutil info` prints when no config file was loaded.
|
||||
const noConfigFile = "(none)"
|
||||
|
||||
// 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 {
|
||||
// Go toolchain version, the module path, the configuration file in use,
|
||||
// and the VCS metadata recorded by the Go toolchain at build time.
|
||||
func newInfoCommand(a *app) *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)
|
||||
return writeBuildInfo(
|
||||
cmd.OutOrStdout(), a.params.Version, a.configFileUsed(),
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// writeBuildInfo writes one "key: value" line per fact to w.
|
||||
func writeBuildInfo(w io.Writer, version string) error {
|
||||
func writeBuildInfo(w io.Writer, version, configFile string) error {
|
||||
info, ok := debug.ReadBuildInfo()
|
||||
if !ok {
|
||||
return ErrNoBuildInfo
|
||||
}
|
||||
|
||||
if configFile == "" {
|
||||
configFile = noConfigFile
|
||||
}
|
||||
|
||||
lines := []string{
|
||||
"version: " + version,
|
||||
"go: " + info.GoVersion,
|
||||
"module: " + info.Main.Path,
|
||||
"config: " + configFile,
|
||||
}
|
||||
|
||||
for _, setting := range info.Settings {
|
||||
|
||||
+57
-124
@@ -1,98 +1,60 @@
|
||||
// Package cbk derives NaCl crypto_box keypairs (Curve25519, XSalsa20,
|
||||
// Poly1305) deterministically from BIP39 mnemonic phrases, and seals and
|
||||
// opens messages with them.
|
||||
// Package cbk derives NaCl crypto_box keypairs (Curve25519) deterministically
|
||||
// from BIP39 mnemonic phrases using BIP85. That is all it does; the
|
||||
// resulting keys are plain 32-byte crypto_box keys for use with
|
||||
// golang.org/x/crypto/nacl/box.
|
||||
//
|
||||
// # Key derivation
|
||||
//
|
||||
// The same mnemonic and passphrase always produce the same keypair:
|
||||
// The same mnemonic and index 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
|
||||
// mnemonic with PBKDF2-HMAC-SHA512 (2048 rounds, salt "mnemonic"),
|
||||
// exactly as a wallet would with an empty passphrase. BIP39
|
||||
// passphrases are deliberately not supported.
|
||||
// 3. The seed becomes a BIP32 master key.
|
||||
// 4. BIP85 (sneak.berlin/go/secret/pkg/bip85) derives 32 bytes of
|
||||
// application entropy from the master key with the HEX application,
|
||||
// at path m/83696968'/128169'/32'/{index}'. Those bytes are the
|
||||
// crypto_box private key.
|
||||
// 5. 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.
|
||||
// Because the entropy comes from BIP85, any BIP85-capable tool given the
|
||||
// same mnemonic reproduces the private key, and the index lets one
|
||||
// mnemonic back any number of independent keypairs.
|
||||
package cbk
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/hkdf"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil/hdkeychain"
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/tyler-smith/go-bip39"
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
|
||||
"sneak.berlin/go/secret/pkg/bip85"
|
||||
"sneak.berlin/go/x/pkg/bip39wordlist"
|
||||
)
|
||||
|
||||
const (
|
||||
// KeySize is the length in bytes of a crypto_box public or private key.
|
||||
// It is also the byte count requested from the BIP85 HEX application.
|
||||
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
|
||||
// MaxIndex is the largest valid Params.Index. BIP85 path components
|
||||
// are hardened BIP32 child indexes, which leaves 31 bits.
|
||||
MaxIndex = hdkeychain.HardenedKeyStart - 1
|
||||
)
|
||||
|
||||
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")
|
||||
)
|
||||
// ErrIndexTooLarge is returned by New when Params.Index exceeds MaxIndex.
|
||||
var ErrIndexTooLarge = errors.New("cbk: index exceeds the BIP32 hardened range")
|
||||
|
||||
// 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[:]
|
||||
@@ -109,10 +71,10 @@ type Params struct {
|
||||
// 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
|
||||
// Index selects which of the keypairs derivable from the mnemonic to
|
||||
// return. It is the final component of the BIP85 derivation path, so
|
||||
// each index yields an independent keypair. Zero is the first.
|
||||
Index uint32
|
||||
}
|
||||
|
||||
// CryptoBoxKeypair is a crypto_box public and private key pair.
|
||||
@@ -121,20 +83,37 @@ type CryptoBoxKeypair struct {
|
||||
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.
|
||||
// New derives the keypair for the mnemonic and index 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) {
|
||||
if p.Index > MaxIndex {
|
||||
return CryptoBoxKeypair{}, fmt.Errorf("%w: %d", ErrIndexTooLarge, p.Index)
|
||||
}
|
||||
|
||||
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)
|
||||
// The seed is always computed with an empty BIP39 passphrase. The
|
||||
// network parameters only choose the version bytes used when a key is
|
||||
// serialized; they do not affect derivation.
|
||||
masterKey, err := hdkeychain.NewMaster(
|
||||
bip39.NewSeed(mnemonic, ""), &chaincfg.MainNetParams,
|
||||
)
|
||||
if err != nil {
|
||||
return CryptoBoxKeypair{}, fmt.Errorf("cbk: deriving private key: %w", err)
|
||||
return CryptoBoxKeypair{}, fmt.Errorf("cbk: deriving BIP32 master key: %w", err)
|
||||
}
|
||||
|
||||
privateKeyHex, err := bip85.DeriveHex(masterKey, KeySize, p.Index)
|
||||
if err != nil {
|
||||
return CryptoBoxKeypair{}, fmt.Errorf("cbk: deriving BIP85 entropy: %w", err)
|
||||
}
|
||||
|
||||
privateKeyBytes, err := hex.DecodeString(privateKeyHex)
|
||||
if err != nil {
|
||||
return CryptoBoxKeypair{}, fmt.Errorf("cbk: decoding BIP85 entropy: %w", err)
|
||||
}
|
||||
|
||||
publicKey, privateKey, err := box.GenerateKey(bytes.NewReader(privateKeyBytes))
|
||||
@@ -148,10 +127,10 @@ func New(p Params) (CryptoBoxKeypair, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// FromMnemonic derives the keypair for mnemonic with an empty passphrase.
|
||||
// It is shorthand for New(Params{Mnemonic: mnemonic}).
|
||||
// FromMnemonic derives the keypair for mnemonic at index zero. It is
|
||||
// shorthand for New(Params{Mnemonic: mnemonic}).
|
||||
func FromMnemonic(mnemonic string) (CryptoBoxKeypair, error) {
|
||||
return New(Params{Mnemonic: mnemonic})
|
||||
return New(Params{Mnemonic: mnemonic, Index: 0})
|
||||
}
|
||||
|
||||
// PublicKeyBytes returns a copy of the public key as a byte slice.
|
||||
@@ -175,49 +154,3 @@ func (kp CryptoBoxKeypair) PublicKeyHex() string {
|
||||
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
|
||||
}
|
||||
+108
-129
@@ -4,83 +4,140 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil/hdkeychain"
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
"github.com/tyler-smith/go-bip39"
|
||||
|
||||
"sneak.berlin/go/secret/pkg/bip85"
|
||||
"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.
|
||||
// The BIP85 specification publishes its test vectors against one master
|
||||
// key. specMnemonic is the BIP39 phrase that produces that master key, so
|
||||
// the whole mnemonic-to-entropy chain this package relies on can be
|
||||
// checked against the specification.
|
||||
const (
|
||||
vectorPublicKeyHex = "db9c1ec248805579d0b0fe2d5a4814f96009f66a11c91260487b79167f881b20"
|
||||
specMnemonic = "install scatter logic circle pencil average fall shoe " +
|
||||
"quantum disease suspect usage"
|
||||
|
||||
vectorPrivateKeyHex = "eec3d5da4e348bd16df93b61c595bb5f75645f9aa468e72513be033a72b3d053"
|
||||
specMasterKey = "xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8" +
|
||||
"kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLjbKb"
|
||||
|
||||
// specHex64 is the specification's HEX application vector for
|
||||
// m/83696968'/128169'/64'/0'.
|
||||
specHex64 = "492db4698cf3b73a5a24998aa3e9d7fa96275d85724a91e7" +
|
||||
"1aa2d645442f878555d078fd1f1f67e368976f04137b1f7a" +
|
||||
"0d19232136ca50c44614af72b5582a5c"
|
||||
)
|
||||
|
||||
// vectorMnemonic returns the standard BIP39 test vector for all-zero
|
||||
// 128-bit entropy.
|
||||
func vectorMnemonic() string {
|
||||
return strings.Repeat("abandon ", vectorRepeats) + "about"
|
||||
// The keys below are the keypair for specMnemonic at index 0, pinned when
|
||||
// the BIP85 derivation was implemented. If this test starts failing, the
|
||||
// derivation has changed and existing keys are no longer reproducible.
|
||||
const (
|
||||
vectorPublicKeyHex = "dbef711b3f340d336eb99cd8d0dbd491" +
|
||||
"e1edce8eaad71cd66be228032a9a743e"
|
||||
|
||||
vectorPrivateKeyHex = "ea3ceb0b02ee8e587779c63f4b7b3a21" +
|
||||
"e950a213f1ec53cab608d13e8796e6dc"
|
||||
)
|
||||
|
||||
func TestSpecMnemonicProducesSpecMasterKey(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
masterKey := specMasterKeyFromMnemonic(t)
|
||||
assert.Equal(t, specMasterKey, masterKey.String())
|
||||
|
||||
hex64, err := bip85.DeriveHex(masterKey, 64, 0)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, specHex64, hex64)
|
||||
}
|
||||
|
||||
func TestNewMatchesPinnedVector(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
keypair, err := cbk.FromMnemonic(vectorMnemonic())
|
||||
keypair, err := cbk.FromMnemonic(specMnemonic)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, vectorPublicKeyHex, keypair.PublicKeyHex())
|
||||
assert.Equal(t, vectorPrivateKeyHex, keypair.PrivateKeyHex())
|
||||
}
|
||||
|
||||
func TestPrivateKeyIsBIP85HexApplicationOutput(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
masterKey := specMasterKeyFromMnemonic(t)
|
||||
|
||||
for _, index := range []uint32{0, 1, 7} {
|
||||
expected, err := bip85.DeriveHex(masterKey, cbk.KeySize, index)
|
||||
require.NoError(t, err)
|
||||
|
||||
keypair, err := cbk.New(cbk.Params{Mnemonic: specMnemonic, Index: index})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, expected, keypair.PrivateKeyHex(), "index %d", index)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewIndexSelectsIndependentKeypairs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
first, err := cbk.New(cbk.Params{Mnemonic: specMnemonic, Index: 0})
|
||||
require.NoError(t, err)
|
||||
|
||||
second, err := cbk.New(cbk.Params{Mnemonic: specMnemonic, Index: 1})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.NotEqual(t, first.PublicKey, second.PublicKey)
|
||||
assert.NotEqual(t, first.PrivateKey, second.PrivateKey)
|
||||
|
||||
fromMnemonic, err := cbk.FromMnemonic(specMnemonic)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, first, fromMnemonic, "FromMnemonic uses index 0")
|
||||
}
|
||||
|
||||
func TestNewRejectsIndexBeyondHardenedRange(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := cbk.New(cbk.Params{Mnemonic: specMnemonic, Index: cbk.MaxIndex + 1})
|
||||
require.ErrorIs(t, err, cbk.ErrIndexTooLarge)
|
||||
|
||||
_, err = cbk.New(cbk.Params{Mnemonic: specMnemonic, Index: cbk.MaxIndex})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestNewNormalizesMnemonic(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
canonical, err := cbk.FromMnemonic(vectorMnemonic())
|
||||
canonical, err := cbk.FromMnemonic(specMnemonic)
|
||||
require.NoError(t, err)
|
||||
|
||||
messy, err := cbk.New(cbk.Params{
|
||||
Mnemonic: " ABANDON\tAbandon " + strings.Repeat("abandon ", 9) +
|
||||
"\nabout\n",
|
||||
Passphrase: "",
|
||||
})
|
||||
words := strings.Fields(specMnemonic)
|
||||
messy := " " + strings.ToUpper(words[0]) + "\t" +
|
||||
strings.Join(words[1:], " ") + "\n"
|
||||
|
||||
normalized, err := cbk.New(cbk.Params{Mnemonic: messy, Index: 0})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, canonical, messy)
|
||||
assert.Equal(t, canonical, normalized)
|
||||
}
|
||||
|
||||
func TestNewPassphraseChangesKeys(t *testing.T) {
|
||||
func TestNewDifferentMnemonicsGiveDifferentKeys(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
plain, err := cbk.FromMnemonic(vectorMnemonic())
|
||||
other, err := bip39wordlist.GenerateMnemonic(bip39wordlist.MaxWordCount)
|
||||
require.NoError(t, err)
|
||||
|
||||
extraWord := "TREZOR"
|
||||
|
||||
withExtraWord, err := cbk.New(cbk.Params{
|
||||
Mnemonic: vectorMnemonic(),
|
||||
Passphrase: extraWord,
|
||||
})
|
||||
specKeypair, err := cbk.FromMnemonic(specMnemonic)
|
||||
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,
|
||||
})
|
||||
otherKeypair, err := cbk.FromMnemonic(other)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, withExtraWord, again, "derivation is deterministic")
|
||||
assert.NotEqual(t, specKeypair.PublicKey, otherKeypair.PublicKey)
|
||||
assert.NotEqual(t, specKeypair.PrivateKey, otherKeypair.PrivateKey)
|
||||
}
|
||||
|
||||
func TestNewRejectsInvalidMnemonic(t *testing.T) {
|
||||
@@ -89,14 +146,17 @@ func TestNewRejectsInvalidMnemonic(t *testing.T) {
|
||||
_, err := cbk.FromMnemonic("abandon bogus about")
|
||||
require.ErrorIs(t, err, bip39wordlist.ErrInvalidWordCount)
|
||||
|
||||
_, err = cbk.FromMnemonic(strings.Repeat("abandon ", vectorRepeats) + "bogus")
|
||||
words := strings.Fields(specMnemonic)
|
||||
words[len(words)-1] = "bogus"
|
||||
|
||||
_, err = cbk.FromMnemonic(strings.Join(words, " "))
|
||||
require.ErrorIs(t, err, bip39wordlist.ErrUnknownWord)
|
||||
}
|
||||
|
||||
func TestKeyAccessors(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
keypair, err := cbk.FromMnemonic(vectorMnemonic())
|
||||
keypair, err := cbk.FromMnemonic(specMnemonic)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, keypair.PublicKey[:], keypair.PublicKeyBytes())
|
||||
@@ -105,99 +165,18 @@ func TestKeyAccessors(t *testing.T) {
|
||||
assert.Len(t, keypair.PrivateKeyBytes(), cbk.KeySize)
|
||||
assert.Equal(t, keypair.PublicKey.Hex(), keypair.PublicKeyHex())
|
||||
assert.Equal(t, keypair.PrivateKey.Hex(), keypair.PrivateKeyHex())
|
||||
assert.Len(t, keypair.PublicKeyHex(), 2*cbk.KeySize)
|
||||
}
|
||||
|
||||
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 {
|
||||
// specMasterKeyFromMnemonic builds the BIP32 master key from specMnemonic
|
||||
// the same way the package does, independently of it.
|
||||
func specMasterKeyFromMnemonic(t *testing.T) *hdkeychain.ExtendedKey {
|
||||
t.Helper()
|
||||
|
||||
mnemonic, err := bip39wordlist.GenerateMnemonic(bip39wordlist.MaxWordCount)
|
||||
seed := bip39.NewSeed(specMnemonic, "")
|
||||
|
||||
masterKey, err := hdkeychain.NewMaster(seed, &chaincfg.MainNetParams)
|
||||
require.NoError(t, err)
|
||||
|
||||
keypair, err := cbk.FromMnemonic(mnemonic)
|
||||
require.NoError(t, err)
|
||||
|
||||
return keypair
|
||||
return masterKey
|
||||
}
|
||||
Executable
+104
@@ -0,0 +1,104 @@
|
||||
#!/bin/sh
|
||||
# script/bootstrap: install all dependencies needed to build and develop
|
||||
# this repo. Idempotent: every install is guarded by a check so already
|
||||
# installed tools are skipped. Base tools, go, node and yarn come from
|
||||
# nix, apt, brew, or apk (detected in that order), with no version and
|
||||
# no hash, because the distribution is trusted. It installs node, yarn
|
||||
# and the node packages only when yarn.lock is in the tree. Assumes
|
||||
# nothing is present.
|
||||
set -eu
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
|
||||
PKGMGR=""
|
||||
SUDO=""
|
||||
APT_UPDATED=""
|
||||
|
||||
detect_pkgmgr() {
|
||||
[ -n "$PKGMGR" ] && return 0
|
||||
if command -v nix-env >/dev/null 2>&1; then
|
||||
PKGMGR="nix"
|
||||
elif command -v apt-get >/dev/null 2>&1; then
|
||||
PKGMGR="apt"
|
||||
elif command -v brew >/dev/null 2>&1; then
|
||||
PKGMGR="brew"
|
||||
elif command -v apk >/dev/null 2>&1; then
|
||||
PKGMGR="apk"
|
||||
else
|
||||
echo "bootstrap: no supported package manager (nix, apt, brew, apk)" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ "$PKGMGR" = "apt" ]; then
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
if [ "$(id -u)" != "0" ]; then
|
||||
SUDO="sudo"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# pkg_install <nix-attr> <apt-pkg> <brew-formula> <apk-pkg>
|
||||
pkg_install() {
|
||||
detect_pkgmgr
|
||||
case "$PKGMGR" in
|
||||
nix) nix-env -iA "nixpkgs.$1" ;;
|
||||
apt)
|
||||
# Package lists may be empty (fresh images); refresh once per run.
|
||||
if [ -z "$APT_UPDATED" ]; then
|
||||
$SUDO env DEBIAN_FRONTEND=noninteractive apt-get update
|
||||
APT_UPDATED=1
|
||||
fi
|
||||
$SUDO env DEBIAN_FRONTEND=noninteractive apt-get install -y "$2"
|
||||
;;
|
||||
brew) brew install "$3" ;;
|
||||
apk) apk add --no-cache "$4" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
missing() {
|
||||
! command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Debian and Ubuntu name the yarn command yarnpkg.
|
||||
run_yarn() {
|
||||
if command -v yarn >/dev/null 2>&1; then
|
||||
yarn "$@"
|
||||
elif command -v yarnpkg >/dev/null 2>&1; then
|
||||
yarnpkg "$@"
|
||||
else
|
||||
echo "no yarn; run script/bootstrap first" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
cd "$ROOT"
|
||||
|
||||
if missing make; then pkg_install gnumake make make make; fi
|
||||
if missing git; then pkg_install git git git git; fi
|
||||
# Go on the host is for script/fmt and script/fmt-check (gofmt) and
|
||||
# for make build; the gates compile inside the Dockerfile's phases.
|
||||
if missing go; then pkg_install go golang-go go go; fi
|
||||
|
||||
# A Dockerfile stage that needs no yarn copies no yarn.lock.
|
||||
if [ -f yarn.lock ]; then
|
||||
if missing node; then pkg_install nodejs nodejs node nodejs; fi
|
||||
if missing yarn && missing yarnpkg; then
|
||||
pkg_install yarn yarnpkg yarn yarn
|
||||
fi
|
||||
|
||||
# Debian 13 and Ubuntu 26.04 package yarn 4, which refuses a yarn 1
|
||||
# yarn.lock, or rewrites it in its own format.
|
||||
yarn_version="$(run_yarn --version)"
|
||||
if [ "${yarn_version%%.*}" != "1" ]; then
|
||||
echo "bootstrap: yarn $yarn_version refuses this yarn 1 yarn.lock;" \
|
||||
"use a distribution that packages yarn 1" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
run_yarn install --frozen-lockfile
|
||||
fi
|
||||
|
||||
echo "bootstrap complete"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
#!/bin/sh
|
||||
# script/check: run all checks (test, lint, fmt-check). Our own
|
||||
# extension to scripts-to-rule-them-all. test and lint are Docker
|
||||
# phases; fmt-check is native, because a formatter writes the working
|
||||
# tree. Must not modify any files.
|
||||
set -eu
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||
|
||||
main() {
|
||||
"$SCRIPT_DIR/test"
|
||||
"$SCRIPT_DIR/lint"
|
||||
"$SCRIPT_DIR/fmt-check"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Executable
+33
@@ -0,0 +1,33 @@
|
||||
#!/bin/sh
|
||||
# script/cibuild: run the CI build. It bootstraps first: a CI runner
|
||||
# checks out and runs this and nothing else, and script/fmt-check runs
|
||||
# the formatter on the host, which a pristine checkout cannot do.
|
||||
# A new CACHEBUST value reruns every RUN step after the ARG CACHEBUST
|
||||
# line, since a cached gate phase is a check that did not run; unlike
|
||||
# --no-cache, it keeps the layers above that line and the cache mounts.
|
||||
# This relies on every stage of the Dockerfile declaring ARG CACHEBUST
|
||||
# before every step that lints, tests or compiles; a stage without that
|
||||
# line is served from cache on an unchanged tree, with no warning.
|
||||
set -eu
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
|
||||
|
||||
main() {
|
||||
cd "$ROOT"
|
||||
"$SCRIPT_DIR/bootstrap"
|
||||
"$SCRIPT_DIR/check"
|
||||
# The version and the tag each get their own line: a failing
|
||||
# command substitution inside an argument does not trip `set -e`,
|
||||
# so the inline form degrades silently to an empty constant. The
|
||||
# VERSION build argument takes precedence over the version a build
|
||||
# stage derives from the .git in the context.
|
||||
version="$(git describe --tags --always --dirty 2>/dev/null || true)"
|
||||
[ -n "$version" ] || version="unknown"
|
||||
tag="$("$SCRIPT_DIR/projectname")"
|
||||
docker build --build-arg CACHEBUST="$(date +%s)-$$" \
|
||||
--build-arg VERSION="$version" \
|
||||
-t "$tag" .
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Executable
+30
@@ -0,0 +1,30 @@
|
||||
#!/bin/sh
|
||||
# script/docker: build the Docker image tagged with the project name.
|
||||
# Identical in all repos; the tag comes from script/projectname.
|
||||
# A new CACHEBUST value reruns every RUN step after the ARG CACHEBUST
|
||||
# line, since a cached gate phase is a check that did not run; unlike
|
||||
# --no-cache, it keeps the layers above that line and the cache mounts.
|
||||
# This relies on every stage of the Dockerfile declaring ARG CACHEBUST
|
||||
# before every step that lints, tests or compiles; a stage without that
|
||||
# line is served from cache on an unchanged tree, with no warning.
|
||||
set -eu
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
|
||||
|
||||
main() {
|
||||
cd "$ROOT"
|
||||
# The version and the tag each get their own line: a failing
|
||||
# command substitution inside an argument does not trip `set -e`,
|
||||
# so the inline form degrades silently to an empty constant. The
|
||||
# VERSION build argument takes precedence over the version a build
|
||||
# stage derives from the .git in the context.
|
||||
version="$(git describe --tags --always --dirty 2>/dev/null || true)"
|
||||
[ -n "$version" ] || version="unknown"
|
||||
tag="$("$SCRIPT_DIR/projectname")"
|
||||
docker build --build-arg CACHEBUST="$(date +%s)-$$" \
|
||||
--build-arg VERSION="$version" \
|
||||
-t "$tag" .
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Executable
+29
@@ -0,0 +1,29 @@
|
||||
#!/bin/sh
|
||||
# script/fmt: format all files (writes): gofmt for Go, prettier for
|
||||
# markdown.
|
||||
set -eu
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
|
||||
GO_DIRS="cmd internal pkg"
|
||||
|
||||
# Debian and Ubuntu name the yarn command yarnpkg.
|
||||
run_yarn() {
|
||||
if command -v yarn >/dev/null 2>&1; then
|
||||
yarn "$@"
|
||||
elif command -v yarnpkg >/dev/null 2>&1; then
|
||||
yarnpkg "$@"
|
||||
else
|
||||
echo "no yarn; run script/bootstrap first" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
cd "$ROOT"
|
||||
# shellcheck disable=SC2086 # GO_DIRS is a word list on purpose
|
||||
gofmt -w $GO_DIRS
|
||||
run_yarn run prettier --write '**/*.md' --tab-width 4 --prose-wrap always
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
#!/bin/sh
|
||||
# script/fmt-check: check formatting (read-only): gofmt for Go, prettier
|
||||
# for markdown.
|
||||
set -eu
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
|
||||
GO_DIRS="cmd internal pkg"
|
||||
|
||||
# Debian and Ubuntu name the yarn command yarnpkg.
|
||||
run_yarn() {
|
||||
if command -v yarn >/dev/null 2>&1; then
|
||||
yarn "$@"
|
||||
elif command -v yarnpkg >/dev/null 2>&1; then
|
||||
yarnpkg "$@"
|
||||
else
|
||||
echo "no yarn; run script/bootstrap first" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
cd "$ROOT"
|
||||
# shellcheck disable=SC2086 # GO_DIRS is a word list on purpose
|
||||
unformatted="$(gofmt -l $GO_DIRS)"
|
||||
if [ -n "$unformatted" ]; then
|
||||
echo "gofmt: the following files need formatting:" >&2
|
||||
echo "$unformatted" >&2
|
||||
exit 1
|
||||
fi
|
||||
run_yarn run prettier --check '**/*.md' --tab-width 4 --prose-wrap always
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
#!/bin/sh
|
||||
# script/install-precommit: install the git pre-commit hook that runs
|
||||
# script/precommit. Our own extension to scripts-to-rule-them-all.
|
||||
set -eu
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
|
||||
main() {
|
||||
cd "$ROOT"
|
||||
hook=".git/hooks/pre-commit"
|
||||
printf '#!/bin/sh\nset -e\nscript/precommit\n' > .git/hooks/pre-commit
|
||||
chmod +x .git/hooks/pre-commit
|
||||
echo "pre-commit hook installed: runs script/precommit"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Executable
+28
@@ -0,0 +1,28 @@
|
||||
#!/bin/sh
|
||||
# script/lint: run the linter. Linting is a phase of the Dockerfile and
|
||||
# this builds that phase alone; the linter is never installed or run on
|
||||
# a developer host, where a shared result cache and a host-global lock
|
||||
# make its answer untrustworthy.
|
||||
#
|
||||
# The phase is not the last stage in the file, so it is built only when
|
||||
# --target names it. A new CACHEBUST value reruns every RUN step after
|
||||
# the ARG CACHEBUST line, since a cached lint layer is a lint that did
|
||||
# not run; unlike --no-cache, it keeps the layers above that line and
|
||||
# the cache mounts. This relies on every stage of the Dockerfile
|
||||
# declaring ARG CACHEBUST before every step that lints, tests or
|
||||
# compiles; a stage without that line is served from cache on an
|
||||
# unchanged tree, with no warning.
|
||||
# --output type=cacheonly writes no image, since nothing uses one.
|
||||
set -eu
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
|
||||
|
||||
main() {
|
||||
cd "$ROOT"
|
||||
docker build --build-arg CACHEBUST="$(date +%s)-$$" \
|
||||
--target lint \
|
||||
--output type=cacheonly .
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Executable
+22
@@ -0,0 +1,22 @@
|
||||
#!/bin/sh
|
||||
# script/precommit: run by the git pre-commit hook; fails the commit if
|
||||
# checks fail. Our own extension to scripts-to-rule-them-all. The Go
|
||||
# extra: go.mod and go.sum must already be tidy, so a commit never
|
||||
# carries a module file that go mod tidy would rewrite.
|
||||
set -eu
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
|
||||
|
||||
main() {
|
||||
cd "$ROOT"
|
||||
go mod tidy
|
||||
if ! git diff --quiet --exit-code -- go.mod go.sum; then
|
||||
echo "precommit: go mod tidy changed go.mod or go.sum;" \
|
||||
"stage them and retry" >&2
|
||||
exit 1
|
||||
fi
|
||||
"$SCRIPT_DIR/check"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Executable
+12
@@ -0,0 +1,12 @@
|
||||
#!/bin/sh
|
||||
# script/projectname: output the name of this project. Our own
|
||||
# extension to scripts-to-rule-them-all. Other scripts that need the
|
||||
# name (e.g. script/docker) call this, so they can stay identical
|
||||
# across all repos.
|
||||
set -eu
|
||||
|
||||
main() {
|
||||
echo "x"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Executable
+13
@@ -0,0 +1,13 @@
|
||||
#!/bin/sh
|
||||
# script/setup: set up the repo for development after a fresh clone:
|
||||
# installs dependencies and the git pre-commit hook.
|
||||
set -eu
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||
|
||||
main() {
|
||||
"$SCRIPT_DIR/bootstrap"
|
||||
"$SCRIPT_DIR/install-precommit"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Executable
+24
@@ -0,0 +1,24 @@
|
||||
#!/bin/sh
|
||||
# script/test: run the test suite. Testing is a phase of the Dockerfile
|
||||
# and this builds that phase alone, on the same terms as script/lint:
|
||||
# --target because a phase that is not the last stage is built only when
|
||||
# named, and a new CACHEBUST value because a cached test layer is a test
|
||||
# that did not run; unlike --no-cache, it keeps the layers above the ARG
|
||||
# CACHEBUST line and the cache mounts. This relies on every stage of the
|
||||
# Dockerfile declaring ARG CACHEBUST before every step that lints, tests
|
||||
# or compiles; a stage without that line is served from cache on an
|
||||
# unchanged tree, with no warning. --output type=cacheonly writes no
|
||||
# image, since nothing uses one.
|
||||
set -eu
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
|
||||
|
||||
main() {
|
||||
cd "$ROOT"
|
||||
docker build --build-arg CACHEBUST="$(date +%s)-$$" \
|
||||
--target test \
|
||||
--output type=cacheonly .
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in new issue
Block a user