Files
secret/pkg/bip85
clawbot 41cea400a7
All checks were successful
check / check (push) Successful in 43s
Update golangci-lint to v2.12.2 with canonical config (#29)
Bumps golangci-lint from v2.1.6 (digest-only pin in the `Dockerfile` lint stage) to v2.12.2, pinned by tag and digest (Debian-based image).

Replaces `.golangci.yml` with the canonical strict config: all linters enabled except the standard disable list (`exhaustruct`, `depguard`, `godot`, `wsl`, `wrapcheck`, `varnamelen`), `lll` at 88, `funlen` 80/50, `cyclop` 15, `dupl` 100, and test files are now linted (the old config had `tests: false`, an enable-only list of ~20 linters, `lll` 120, and a blanket exclusion of `internal/macse`).

The stricter config surfaced ~1550 findings, all fixed:

- `wsl_v5` (439) / `nlreturn` (24): blank-line insertions
- `lll` (309): line wrapping at 88 columns; long literals split with `+` concatenation, values unchanged
- `noinlineerr` (130): `if err := ...` split into assignment plus check
- `paralleltest` (116): `t.Parallel()` added to tests without shared state; reasoned `//nolint` where `t.Setenv` or shared fixtures forbid it
- `err113` (97): package-level sentinel errors (new `internal/vault/errors.go`), `%w` wrapping, `errors.Is`
- `perfsprint` (74) / `modernize` (39) / `intrange`: `strconv`, `errors.New`, `slices.Contains`, `any`, `SplitSeq`
- `goconst` (40) / `dupword` (41) / `testifylint` (42) / `thelper` (33): constants, assertion fixes, `t.Helper()`
- `noctx` (22): `exec.CommandContext` for gpg/CLI invocations
- `testpackage` (18): black-box tests moved to `_test` packages where they use only exported identifiers; white-box files carry a reasoned `//nolint`
- `funlen`/`cyclop`/`gocognit`/`nestif`/`dupl`: behavior-preserving helper extraction
- assorted singletons: `gosec`, `gosmopolitan`, `funcorder`, `nonamedreturns`, `makezero`, `prealloc`, `godox`, `nolintlint`, `ireturn`, `nilnil`, `gochecknoinits`

## User-visible strings

**None changed.** Every error message this branch composes is byte-identical to the one `main` composes.

The `err113` sentinels are shaped so `fmt.Errorf` reassembles the original text around them: the sentinel carries the fixed words and the caller supplies the interpolated value in the position it has always occupied. Where the value sits mid-sentence the sentinel holds only a fragment (e.g. `vault.ErrVaultNotFound` is `"does not exist"`, composed by its caller as `vault <name> does not exist`); each such sentinel documents the message it participates in.

Verified mechanically, not by inspection: every `fmt.Errorf` and `errors.New` call site in both trees is parsed, the `Error()` text of any sentinel passed to `%w` is substituted in, and the resulting sets of composed message templates are compared. All 350 templates `main` produces are still produced, character for character. The set of lost or altered messages is empty.

## `unlocker list`

`findUnlockerIDByMetadata` returns `(string, error)` rather than signalling failure with an empty ID, so an unreadable `unlockers.d` is no longer indistinguishable from "no matching entry". `UnlockersList` skips such an entry with a warning naming the directory — its behavior before the scan was extracted into a helper — instead of emitting a row under a synthesized fallback ID that no `unlocker remove` or `unlocker select` can match and that suppresses the current-unlocker marker. The duplicate-check and shell-completion callers skip on the same condition, matching their pre-extraction behavior. Covered by `internal/cli/unlockers_list_test.go`.

`TODO.md` records the change plus follow-ups (version-completion TODOs formerly in code comments, darwin-gated files exceeding 88 columns that Linux CI does not lint).

`make check` is green and the pinned v2.12.2 image reports `0 issues.` Note the test suite needs the memlock ulimit from `script/cibuild` for the 10MB memguard test; that requirement is pre-existing.

Not changed: `script/bootstrap` installs golangci-lint via the system package manager (no version pin to bump), and `script/lint` invokes whatever `golangci-lint` is on PATH. golangci-lint v2.12 deprecates `gomodguard` in favor of `gomodguard_v2` (warning only); the canonical config owns that decision.

Co-authored-by: sneak <sneak@sneak.berlin>
Reviewed-on: #29
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-08-10 15:23:33 +02:00
..
2025-05-28 14:06:29 -07:00
2025-05-28 14:06:29 -07:00

BIP85 - Deterministic Entropy From BIP32 Keychains

This package implements BIP85, which allows for deterministic derivation of entropy from a BIP32 master key. This enables a single seed to generate multiple wallet keys, mnemonics, and random values in a fully deterministic way.

Overview

BIP85 enables a variety of use cases:

  • Generate multiple BIP39 mnemonic seeds from a single master key
  • Derive Bitcoin HD wallet seeds (WIF format)
  • Create extended private keys (XPRV)
  • Generate deterministic random values for hex values and passwords

Usage Examples

Initialization

import (
    "fmt"
    "git.eeqj.de/sneak/secret/pkg/bip85"
    "github.com/btcsuite/btcd/btcutil/hdkeychain"
)

// Parse an existing master key
masterKeyStr := "xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLjbKb"
masterKey, err := bip85.ParseMasterKey(masterKeyStr)
if err != nil {
    panic(err)
}

Derive BIP39 Mnemonic

// Parameters:
// - language (0 = English, 1 = Japanese, 2 = Korean, 3 = Spanish, etc.)
// - number of words (12, 15, 18, 21, or 24)
// - index (allows multiple seeds of the same type)
entropy, err := bip85.DeriveBIP39Entropy(masterKey, 0, 12, 0)
if err != nil {
    panic(err)
}

// Use the entropy with github.com/tyler-smith/go-bip39
// to generate a mnemonic
mnemonic, err := bip39.NewMnemonic(entropy)
if err != nil {
    panic(err)
}
fmt.Println("12-word BIP39 mnemonic:", mnemonic)

Derive HD-WIF Key

// Create a WIF format key for Bitcoin Core's hdseed
wif, err := bip85.DeriveWIFKey(masterKey, 0)
if err != nil {
    panic(err)
}
fmt.Println("WIF Key:", wif)

Derive XPRV

// Create an extended private key (XPRV)
xprv, err := bip85.DeriveXPRV(masterKey, 0)
if err != nil {
    panic(err)
}
fmt.Println("XPRV:", xprv.String())

Generate Hex Data

// Generate arbitrary hex data (16-64 bytes)
hex, err := bip85.DeriveHex(masterKey, 32, 0)
if err != nil {
    panic(err)
}
fmt.Println("32 bytes of hex:", hex)

DRNG (Deterministic Random Number Generator)

// First derive entropy
path := "m/83696968'/0'/0'"
entropy, err := bip85.DeriveBIP85Entropy(masterKey, path)
if err != nil {
    panic(err)
}

// Create a deterministic random number generator
drng := bip85.NewBIP85DRNG(entropy)

// Read arbitrary amount of random bytes
buffer := make([]byte, 32)
_, err = drng.Read(buffer)
if err != nil {
    panic(err)
}
fmt.Printf("Random bytes: %x\n", buffer)

BIP85 Paths

The derivation paths follow the format:

m/83696968'/{app}'/{parameters}

Where:

  • 83696968' is the BIP85 root path (BIP in ASCII)
  • {app}' is the application number:
    • 39' for BIP39 mnemonics
    • 2' for HD-WIF keys
    • 32' for XPRV
    • 128169' for HEX data
    • 707764' for Base64 passwords
    • 707785' for Base85 passwords
    • 828365' for RSA keys
  • {parameters} are application-specific parameters

Test Vectors

This implementation passes all the test vectors from the BIP85 specification:

  • Basic test cases
  • BIP39 12, 18, and 24 word mnemonics
  • HD-WIF keys
  • XPRV
  • SHAKE256 DRNG output

The implementation is also compatible with the Python reference implementation's test vectors for the DRNG functionality.

Run the tests with verbose output to see the test vectors and results:

go test -v git.eeqj.de/sneak/secret/pkg/bip85

References