All checks were successful
check / check (push) Successful in 2m0s
- Replace .golangci.yml with the canonical strict config (all linters enabled except the standard disable list; lll 88, funlen 80/50, cyclop 15, dupl 100; test files now linted) - Pin the Dockerfile lint stage to golangci/golangci-lint:v2.12.2 by tag and digest (Debian-based) - Fix all ~1550 findings surfaced by the new config: line wrapping, wsl_v5/nlreturn blank lines, noinlineerr splits, err113 sentinel errors, perfsprint/modernize rewrites, goconst constants, thelper, testifylint, noctx CommandContext, testpackage conversions, t.Parallel() where safe, and complexity/dupl helper extraction - Record the change and follow-up items in TODO.md User-visible strings -------------------- No user-visible string changes remain. Every error message this branch composes is byte-identical to the one main composes. The err113 sentinels are shaped so that fmt.Errorf reassembles the original text around them: a sentinel carries the fixed words of the message and the caller supplies the interpolated value in the position it has always occupied. Where the value sits in the middle of the sentence the sentinel therefore holds only a fragment (for example 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 rather than by inspection: every fmt.Errorf and errors.New call site in both trees was parsed, the Error() text of any sentinel passed to %w substituted in, and the resulting sets of composed message templates compared. All 350 templates main produces are still produced, character for character; the set of messages lost or altered is empty. unlocker list ------------- findUnlockerIDByMetadata now returns (string, error) instead of signalling failure with an empty ID. An unreadable unlockers.d is no longer indistinguishable from "no matching entry", so UnlockersList skips the entry with a warning naming the directory, as it did before the scan was extracted into a helper, rather than 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 tests in internal/cli/unlockers_list_test.go.
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 mnemonics2'for HD-WIF keys32'for XPRV128169'for HEX data707764'for Base64 passwords707785'for Base85 passwords828365'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