check / check (push) Successful in 1m3s
script/ holds the entrypoints and the Makefile targets shim to them. Linting and testing are phases of the Dockerfile, built alone by script/lint and script/test with a fresh CACHEBUST value and cache mounts for Go's build cache and golangci-lint's; the build stage depends on both, and the runtime stage ships the binary and the source without .git. Formatting stays on the host in script/fmt and script/fmt-check (gofmt and the prettier pinned in yarn.lock). script/cibuild bootstraps, runs script/check and builds the tagged image, and the Gitea workflow calls it. script/precommit refuses a commit whose go.mod or go.sum is not tidy. cgo stays off, so the test phase runs on the alpine Go image without -race; that is the one departure from the canonical Go Dockerfile. Re-vendored from the prompts repository's next branch: REPO_POLICIES.md (2026-11-04), .golangci.yml (byte-identical, so the local exhaustruct_v5 addition is gone now that canonical carries it), .gitea/workflows/check.yml, .editorconfig, .gitignore and .dockerignore, the last two followed by this repository's own entries. AGENTS.md holds the repository-specific guidance for agents. The README gains the Entrypoints section and describes the new gates.
302 lines
12 KiB
Markdown
302 lines
12 KiB
Markdown
# x
|
|
|
|
## Description
|
|
|
|
`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 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/`.
|
|
|
|
## Getting Started
|
|
|
|
### Install the CLI
|
|
|
|
```sh
|
|
go install sneak.berlin/go/x/cmd/sutil@latest
|
|
```
|
|
|
|
Or, from a checkout:
|
|
|
|
```sh
|
|
make build # builds ./sutil with the git version embedded
|
|
make install # installs sutil into $(go env GOBIN)
|
|
```
|
|
|
|
### Use the CLI
|
|
|
|
```sh
|
|
# An endless stream of fast pseudorandom bytes; stop it with Ctrl-C.
|
|
sutil generate random | pv > /dev/null
|
|
|
|
# The same, but deterministic: the same seed always produces the same bytes.
|
|
sutil generate random --count 1073741824 "my seed" > testdata.bin
|
|
|
|
# A memorable password: three dictionary words, each followed by a digit,
|
|
# joined by a randomly chosen delimiter. Requires a system word list.
|
|
sutil generate password
|
|
# marble7-sprout2-velvet9
|
|
|
|
# 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
|
|
|
|
```go
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"sneak.berlin/go/x/pkg/bip39wordlist"
|
|
"sneak.berlin/go/x/pkg/cbk"
|
|
)
|
|
|
|
func main() {
|
|
mnemonic, err := bip39wordlist.GenerateMnemonic(bip39wordlist.MaxWordCount)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
// The same mnemonic 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)
|
|
}
|
|
|
|
fmt.Println(keypair.PublicKeyHex())
|
|
}
|
|
```
|
|
|
|
## Rationale
|
|
|
|
- **Reusable odds and ends need a home.** Several small packages are worth
|
|
sharing between projects but are too small to each deserve a repository, CI
|
|
pipeline, and release cadence of their own. This module collects them under
|
|
one import path.
|
|
- **`/dev/urandom` is slow.** Operating system random devices are tuned for
|
|
small requests of key material, not for filling a disk or producing gigabytes
|
|
of test data. `sutil generate random` streams pseudorandom bytes at close to
|
|
memory bandwidth, and can do so deterministically from a seed so test data is
|
|
reproducible.
|
|
- **Keys you can carry in your head.** A crypto_box keypair derived from a BIP39
|
|
mnemonic can be regenerated anywhere from a phrase a human can write down or
|
|
memorize, with no key file to lose. `pkg/cbk` makes that derivation explicit
|
|
and reproducible.
|
|
- **Passwords you can type.** Three dictionary words with digits and a delimiter
|
|
are far easier to transcribe than line noise, and with the macOS word list
|
|
they still carry about 62 bits of entropy.
|
|
|
|
## Design
|
|
|
|
### Layout
|
|
|
|
| Path | Purpose |
|
|
| ---------------------- | --------------------------------------------------- |
|
|
| `cmd/sutil/` | `package main`; the whole entrypoint is a few lines |
|
|
| `internal/cli/` | The cobra command tree, built by `cli.New` |
|
|
| `internal/randstream/` | Fast seeded pseudorandom byte stream |
|
|
| `internal/pwgen/` | Password generator over the system dictionary |
|
|
| `pkg/bip39wordlist/` | BIP39 English wordlist and mnemonic helpers |
|
|
| `pkg/cbk/` | crypto_box keypairs derived from BIP39 mnemonics |
|
|
|
|
The split between `pkg/` and `internal/` is deliberate and should be kept:
|
|
`pkg/` holds only packages designed to be imported by other programs, with a
|
|
stable, documented API. Anything that exists to support `sutil` goes under
|
|
`internal/`, even if it would be harmless to export, so that the public surface
|
|
of the module stays small and intentional.
|
|
|
|
The command tree holds no global state: `cli.New` builds every command from a
|
|
`Params` struct, so tests construct it repeatedly with captured input and
|
|
output, and nothing happens in `init` functions. 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) 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"`), 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.
|
|
|
|
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, 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 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`
|
|
|
|
The BIP39 English wordlist, embedded from `english.txt` and checked against the
|
|
reference implementation in the tests, plus mnemonic helpers:
|
|
|
|
- `ValidateWordlist(mnemonic)` checks word count, every word, and the checksum,
|
|
ignoring letter case and whitespace. It reports what is wrong with sentinel
|
|
errors (`ErrInvalidWordCount`, `ErrUnknownWord` naming the word and its
|
|
position, `ErrInvalidChecksum`).
|
|
- `NormalizeWordlist(mnemonic)` returns the canonical lowercase,
|
|
single-space-separated form, and refuses anything invalid.
|
|
- `GenerateMnemonic(wordCount)` makes a new mnemonic of 12, 15, 18, 21, or 24
|
|
words from `crypto/rand`.
|
|
- `Contains(word)`, `ValidWordCount(n)`, and the `WordlistEnglish` and
|
|
`Wordlist` tables.
|
|
|
|
### `internal/randstream`
|
|
|
|
An `io.Reader` that expands a 32-byte key with the ChaCha8 generator from
|
|
`math/rand/v2`. With a seed string the key is its SHA-256 and the stream is
|
|
deterministic; without one the key comes from `crypto/rand`, which is the only
|
|
time the operating system's random source is touched. The output is for bulk
|
|
data, not for cryptographic keys.
|
|
|
|
### `internal/pwgen`
|
|
|
|
Produces passwords of the form
|
|
|
|
```
|
|
<word><digit><delim><word><digit><delim><word><digit>
|
|
```
|
|
|
|
where the delimiter is drawn once per password from `.-_|~+=:,!@#%^&*` and used
|
|
in both positions. Words come from the system word list
|
|
(`/usr/share/dict/words`) through `github.com/kavfixnel/words`, filtered to
|
|
lowercase ASCII words of four to eight letters, which drops proper nouns and
|
|
abbreviations. All choices use `crypto/rand`. On a system without a word list,
|
|
`sutil generate password` panics rather than falling back to something weaker;
|
|
the upstream library is only known to work on macOS.
|
|
|
|
### CLI
|
|
|
|
```
|
|
sutil
|
|
├── version print the build version
|
|
├── info print version, Go version, module, VCS metadata
|
|
├── generate (gen)
|
|
│ ├── random [seedstr] stream pseudorandom bytes (--count N to stop)
|
|
│ └── password (pw) print a memorable password
|
|
```
|
|
|
|
`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 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`.
|
|
|
|
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 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: 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 build # build ./sutil on the host
|
|
make docker # build the image tagged x
|
|
```
|
|
|
|
## TODO
|
|
|
|
Nothing outstanding.
|
|
|
|
## License
|
|
|
|
WTFPL. See `LICENSE`.
|
|
|
|
## Author
|
|
|
|
[@sneak](https://sneak.berlin)
|
|
<[sneak@sneak.berlin](mailto:sneak@sneak.berlin)>
|