Files
secret/pkg/bip85/bip85_test.go
sneak 397011a592
All checks were successful
check / check (push) Successful in 2m0s
Update golangci-lint to v2.12.2 with canonical config (closes #30)
- 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.
2026-08-09 02:00:27 +00:00

1166 lines
29 KiB
Go

//nolint:gosec // G101: Test file contains BIP85 test vectors, not real credentials
package bip85_test
import (
"bytes"
"encoding/hex"
"fmt"
"strings"
"testing"
"git.eeqj.de/sneak/secret/pkg/bip85"
"github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/tyler-smith/go-bip39"
)
const (
// Test master BIP32 root key from the BIP85 specification
testMasterKey = "xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8" +
"kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLjbKb"
// Test Case 1 - Basic entropy derivation with path m/83696968'/0'/0'
testCase1Path = "m/83696968'/0'/0'"
testCase1ExpectedDerivedKey = "cca20ccb0e9a90feb0912870c3323b24" +
"874b0ca3d8018c4b96d0b97c0e82ded0"
testCase1ExpectedEntropy = "efecfbccffea313214232d29e71563d941229afb4338c21f" +
"9517c41aaa0d16f00b83d2a09ef747e7a64e8e2bd5a14869" +
"e693da66ce94ac2da570ab7ee48618f7"
// Test Case 2 - Basic entropy derivation with path m/83696968'/0'/1'
testCase2Path = "m/83696968'/0'/1'"
testCase2ExpectedDerivedKey = "503776919131758bb7de7beb6c0ae248" +
"94f4ec042c26032890c29359216e21ba"
testCase2ExpectedEntropy = "70c6e3e8ebee8dc4c0dbba66076819bb8c09672527c4277c" +
"a8729532ad711872218f826919f6b67218adde99018a6df9" +
"095ab2b58d803b5b93ec9802085a690e"
// BIP85-DRNG-SHAKE256 test vector
drngTestPath = "m/83696968'/0'/0'"
drngExpected80Bytes = "b78b1ee6b345eae6836c2d53d33c64cdaf9a6964" +
"87be81b03e822dc84b3f1cd883d7559e53d175f2" +
"43e4c349e822a957bbff9224bc5dde9492ef54e8" +
"a439f6bc8c7355b87a925a37ee405a7502991111"
// Python DRNG test vectors
pythonDRNG50BytesExpected = "b78b1ee6b345eae6836c2d53d33c64cdaf9a6964" +
"87be81b03e822dc84b3f1cd883d7559e53d175f2" +
"43e4c349e822a957bbff"
pythonDRNG100BytesExpected = "9224bc5dde9492ef54e8a439f6bc8c7355b87a92" +
"5a37ee405a7502991111cd2dddaf1883f4e962ab" +
"f4fb4b31cd28d5cf6b14f6ddcc9c19fd56d7f960" +
"a4b27f1d423a55dda4865aa6ddd6b4c26f18d400" +
"bb0a593e6c785d6d7e28c9c64608624318eddc01"
pythonDRNG150BytesExpected = "23750caa2a271f35faa6a3ca292b4be357404eca" +
"6842c69a3717dc3e41f7b38c67be492395b32221" +
"470aa08a2c489018c635a175f731245330e1f470" +
"91dbfb26f2923d10bd2e09280bffd1d94eb2a88f" +
"964aeb1774da04aad3bb1fdde0f77cd5ca79617a" +
"e317375417a51339523057bebef434c440030389" +
"0332e458425242f56a4293dad4f632b82713467b" +
"18ed6e1dab633220523d"
pythonDRNG20BytesExpected = "b78b1ee6b345eae6836c2d53d33c64cdaf9a6964"
pythonDRNG25BytesExpected = "87be81b03e822dc84b3f1cd883d7559e53d175f243e4c349e8"
// BIP39 12 English words test vector
bip39_12WordsPath = "m/83696968'/39'/0'/12'/0'"
bip39_12WordsExpectedEntropy = "6250b68daf746d12a24d58b4787a714b"
bip39_12WordsExpectedMnemonic = "girl mad pet galaxy egg matter matrix prison " +
"refuse sense ordinary nose"
// BIP39 18 English words test vector
bip39_18WordsPath = "m/83696968'/39'/0'/18'/0'"
bip39_18WordsExpectedEntropy = "938033ed8b12698449d4bbca3c853c66b293ea1b1ce9d9dc"
bip39_18WordsExpectedMnemonic = "near account window bike charge season chef " +
"number sketch tomorrow excuse sniff circle vital hockey " +
"outdoor supply token"
// BIP39 24 English words test vector
bip39_24WordsPath = "m/83696968'/39'/0'/24'/0'"
bip39_24WordsExpectedEntropy = "ae131e2312cdc61331542efe0d1077ba" +
"c5ea803adf24b313a4f0e48e9c51f37f"
bip39_24WordsExpectedMnemonic = "puppy ocean match cereal symbol another " +
"shed magic wrap hammer bulb intact gadget divorce twin tonight " +
"reason outdoor destroy simple truth cigar social volcano"
// HD-Seed WIF test vector
hdWifPath = "m/83696968'/2'/0'"
hdWifExpectedEntropy = "7040bb53104f27367f317558e78a994a" +
"da7296c6fde36a364e5baf206e502bb1"
hdWifExpectedWIF = "Kzyv4uF39d4Jrw2W7UryTHwZr1zQVNk4dAFyqE6BuMrMh1Za7uhp"
// XPRV test vector
xprvPath = "m/83696968'/32'/0'"
xprvExpectedKey = "xprv9s21ZrQH143K2srSbCSg4m4kLvPMzcWydgmKEnMmoZUurYuBuYG46c6" +
"P71UGXMzmriLzCCBvKQWBUv3vPB3m1SATMhp3uEjXHJ42jFg7myX"
// HEX test vector
hexPath = "m/83696968'/128169'/64'/0'"
hexExpectedEntropy = "492db4698cf3b73a5a24998aa3e9d7fa96275d85724a91e7" +
"1aa2d645442f878555d078fd1f1f67e368976f04137b1f7a" +
"0d19232136ca50c44614af72b5582a5c"
// PWD Base64 test vector
pwdBase64Path = "m/83696968'/707764'/21'/0'"
pwdBase64ExpectedEntropy = "74a2e87a9ba0cdd549bdd2f9ea880d554c6c355b08ed2508" +
"8cfa88f3f1c4f74632b652fd4a8f5fda43074c6f6964a375" +
"3b08bb5210c8f5e75c07a4c2a20bf6e9"
pwdBase64ExpectedPassword = "dKLoepugzdVJvdL56ogNV"
// PWD Base85 test vector
pwdBase85Path = "m/83696968'/707785'/12'/0'"
pwdBase85ExpectedEntropy = "f7cfe56f63dca2490f65fcbf9ee63dcd85d18f751b6b5e1c" +
"1b8733af6459c904a75e82b4a22efff9b9e69de2144b293a" +
"a8714319a054b6cb55826a8e51425209"
pwdBase85ExpectedPassword = "_s`{TW89)i4`"
// Test keys for parsing tests
testInvalidMasterKey = "xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8" +
"kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLjbXX"
testTestnetMasterKey = "tprv8ZgxMBicQKsPeWHBt7a68nPnvgTnuDhUgDWC8wZCgA8GahrQ3f3uWpq7" +
"wE7Uc1dLBnCe1hhCZ886K6ND37memRDWqsA9HgSKDXtwh2Qxo6J"
)
// logTestVector logs test information in a cleaner, more concise format
func logTestVector(t *testing.T, title string) {
t.Helper()
t.Logf("=== TEST: %s ===", title)
}
// mustParseTestMasterKey parses the shared test master key.
func mustParseTestMasterKey(t *testing.T) *hdkeychain.ExtendedKey {
t.Helper()
masterKey, err := bip85.ParseMasterKey(testMasterKey)
if err != nil {
t.Fatalf("Failed to parse master key: %v", err)
}
return masterKey
}
// checkDerivedChildKey derives the child key at path and compares it
// against the expected hex value.
func checkDerivedChildKey(t *testing.T, path, expected string) {
t.Helper()
masterKey := mustParseTestMasterKey(t)
t.Logf("Deriving key for path: %s", path)
derivedKeyBytes, err := bip85.DeriveChildKey(masterKey, path)
if err != nil {
t.Fatalf("Failed to derive child key: %v", err)
}
derivedKeyHex := hex.EncodeToString(derivedKeyBytes)
t.Logf("EXPECTED: %s", expected)
t.Logf("ACTUAL: %s", derivedKeyHex)
if derivedKeyHex != expected {
t.Errorf(
"Expected derived key bytes %s, got %s",
expected,
derivedKeyHex,
)
} else {
t.Logf("RESULT: PASS")
}
}
// TestDerivedKey tests the derived key directly
func TestDerivedKey(t *testing.T) {
t.Parallel()
logTestVector(t, "Derived Child Keys")
// Test case 1
checkDerivedChildKey(t, testCase1Path, testCase1ExpectedDerivedKey)
// Test case 2
checkDerivedChildKey(t, testCase2Path, testCase2ExpectedDerivedKey)
}
// runEntropyVectorTest checks a BIP85 entropy derivation test vector.
func runEntropyVectorTest(t *testing.T, title, path, expectedEntropy string) {
t.Helper()
logTestVector(t, title)
masterKey := mustParseTestMasterKey(t)
t.Logf("Test path: %s", path)
entropy, err := bip85.DeriveBIP85Entropy(masterKey, path)
if err != nil {
t.Fatalf("Failed to derive entropy: %v", err)
}
derivedEntropyHex := hex.EncodeToString(entropy)
t.Logf("EXPECTED: %s", expectedEntropy)
t.Logf("ACTUAL: %s", derivedEntropyHex)
if derivedEntropyHex != expectedEntropy {
t.Errorf(
"Expected derived entropy %s, got %s",
expectedEntropy,
derivedEntropyHex,
)
} else {
t.Logf("RESULT: PASS")
}
}
// TestCase1 tests the first test vector from the BIP85 specification
func TestCase1(t *testing.T) {
t.Parallel()
runEntropyVectorTest(t, "Test Case 1", testCase1Path, testCase1ExpectedEntropy)
}
// TestCase2 tests the second test vector from the BIP85 specification
func TestCase2(t *testing.T) {
t.Parallel()
runEntropyVectorTest(t, "Test Case 2", testCase2Path, testCase2ExpectedEntropy)
}
// runBIP39VectorTest checks a BIP39 mnemonic derivation test vector.
func runBIP39VectorTest(
t *testing.T,
title, path string,
words uint32,
expectedEntropy, expectedMnemonic string,
) {
t.Helper()
logTestVector(t, title)
masterKey := mustParseTestMasterKey(t)
t.Logf("Path: %s", path)
t.Logf("Parameters: Language=English(0), Words=%d, Index=0", words)
// Derive the BIP39 mnemonic entropy
entropy, err := bip85.DeriveBIP39Entropy(masterKey, 0, words, 0)
if err != nil {
t.Fatalf("Failed to derive BIP39 entropy: %v", err)
}
derivedEntropyHex := hex.EncodeToString(entropy)
t.Logf("EXPECTED ENTROPY: %s", expectedEntropy)
t.Logf("ACTUAL ENTROPY: %s", derivedEntropyHex)
if derivedEntropyHex != expectedEntropy {
t.Errorf(
"Expected derived entropy %s, got %s",
expectedEntropy,
derivedEntropyHex,
)
} else {
t.Logf("ENTROPY MATCH: PASS")
}
// Convert entropy to mnemonic
mnemonic, err := bip39.NewMnemonic(entropy)
if err != nil {
t.Fatalf("Failed to create mnemonic: %v", err)
}
t.Logf("EXPECTED MNEMONIC: %s", expectedMnemonic)
t.Logf("ACTUAL MNEMONIC: %s", mnemonic)
if mnemonic != expectedMnemonic {
t.Errorf(
"Expected mnemonic '%s', got '%s'",
expectedMnemonic,
mnemonic,
)
} else {
t.Logf("MNEMONIC MATCH: PASS")
}
}
// TestBIP39_12EnglishWords tests the BIP39 12 English words test vector
func TestBIP39_12EnglishWords(t *testing.T) {
t.Parallel()
runBIP39VectorTest(
t,
"BIP39 12 English Words",
bip39_12WordsPath,
12,
bip39_12WordsExpectedEntropy,
bip39_12WordsExpectedMnemonic,
)
}
// TestBIP39_18EnglishWords tests the BIP39 18 English words test vector
func TestBIP39_18EnglishWords(t *testing.T) {
t.Parallel()
runBIP39VectorTest(
t,
"BIP39 18 English Words",
bip39_18WordsPath,
18,
bip39_18WordsExpectedEntropy,
bip39_18WordsExpectedMnemonic,
)
}
// TestBIP39_24EnglishWords tests the BIP39 24 English words test vector
func TestBIP39_24EnglishWords(t *testing.T) {
t.Parallel()
runBIP39VectorTest(
t,
"BIP39 24 English Words",
bip39_24WordsPath,
24,
bip39_24WordsExpectedEntropy,
bip39_24WordsExpectedMnemonic,
)
}
// TestHD_WIF tests the WIF test vector
func TestHD_WIF(t *testing.T) {
t.Parallel()
logTestVector(t, "HD-Seed WIF")
masterKey := mustParseTestMasterKey(t)
// First verify the entropy derivation
t.Logf("Path: %s", hdWifPath)
entropy, err := bip85.DeriveBIP85Entropy(masterKey, hdWifPath)
if err != nil {
t.Fatalf("Failed to derive entropy: %v", err)
}
// Expected entropy from BIP85 spec; WIF uses first 32 bytes
derivedEntropyHex := hex.EncodeToString(entropy[:32])
if derivedEntropyHex != hdWifExpectedEntropy {
t.Errorf(
"Entropy mismatch!\nExpected: %s\nGot: %s",
hdWifExpectedEntropy,
derivedEntropyHex,
)
}
// Now test the WIF derivation
wif, err := bip85.DeriveWIFKey(masterKey, 0)
if err != nil {
t.Fatalf("Failed to derive WIF key: %v", err)
}
t.Logf("EXPECTED WIF: %s", hdWifExpectedWIF)
t.Logf("ACTUAL WIF: %s", wif)
if wif != hdWifExpectedWIF {
t.Errorf("Expected WIF %s, got %s", hdWifExpectedWIF, wif)
} else {
t.Logf("RESULT: PASS")
}
}
// TestXPRV tests the XPRV test vector
func TestXPRV(t *testing.T) {
t.Parallel()
logTestVector(t, "XPRV")
masterKey := mustParseTestMasterKey(t)
t.Logf("Path: %s", xprvPath)
derivedKey, err := bip85.DeriveXPRV(masterKey, 0)
if err != nil {
t.Fatalf("Failed to derive XPRV: %v", err)
}
derivedXPRV := derivedKey.String()
t.Logf("EXPECTED XPRV: %s", xprvExpectedKey)
t.Logf("ACTUAL XPRV: %s", derivedXPRV)
if derivedXPRV != xprvExpectedKey {
t.Errorf("Expected XPRV %s, got %s", xprvExpectedKey, derivedXPRV)
} else {
t.Logf("RESULT: PASS")
}
}
// TestDRNG_SHAKE256 tests the BIP85-DRNG-SHAKE256 test vector
func TestDRNG_SHAKE256(t *testing.T) {
t.Parallel()
logTestVector(t, "DRNG-SHAKE256")
masterKey := mustParseTestMasterKey(t)
// Derive entropy for the DRNG
entropy, err := bip85.DeriveBIP85Entropy(masterKey, drngTestPath)
if err != nil {
t.Fatalf("Failed to derive entropy: %v", err)
}
// Create DRNG
drng := bip85.NewBIP85DRNG(entropy)
// Read 80 bytes
buffer := make([]byte, 80)
n, err := drng.Read(buffer)
if err != nil {
t.Fatalf("Failed to read from DRNG: %v", err)
}
if n != 80 {
t.Errorf("Expected to read 80 bytes, got %d", n)
}
hexOutput := hex.EncodeToString(buffer)
if !strings.EqualFold(hexOutput, drngExpected80Bytes) {
t.Errorf(
"Expected DRNG output:\n%s\n\nGot:\n%s",
drngExpected80Bytes,
hexOutput,
)
}
}
// readDRNGHex reads size bytes from drng and returns the hex encoding.
func readDRNGHex(t *testing.T, drng *bip85.DRNG, size int, label string) string {
t.Helper()
buf := make([]byte, size)
_, err := drng.Read(buf)
if err != nil {
t.Fatalf("Failed to read %s from DRNG: %v", label, err)
}
return hex.EncodeToString(buf)
}
// TestPythonDRNGVectors tests the DRNG vectors from the Python implementation
func TestPythonDRNGVectors(t *testing.T) {
t.Parallel()
logTestVector(t, "Python DRNG Vectors")
masterKey := mustParseTestMasterKey(t)
// Derive entropy for the DRNG
entropy, err := bip85.DeriveBIP85Entropy(masterKey, drngTestPath)
if err != nil {
t.Fatalf("Failed to derive entropy: %v", err)
}
// Create DRNG
drng := bip85.NewBIP85DRNG(entropy)
// Test vector 1: Read 50 bytes
actual1 := readDRNGHex(t, drng, 50, "50 bytes")
if actual1 != pythonDRNG50BytesExpected {
t.Errorf(
"Test vector 1 failed. Expected:\n%s\n\nGot:\n%s",
pythonDRNG50BytesExpected,
actual1,
)
}
// Test vector 2: Read 100 bytes
actual2 := readDRNGHex(t, drng, 100, "100 bytes")
if actual2 != pythonDRNG100BytesExpected {
t.Errorf(
"Test vector 2 failed. Expected:\n%s\n\nGot:\n%s",
pythonDRNG100BytesExpected,
actual2,
)
}
// Test vector 3: Read 150 bytes
actual3 := readDRNGHex(t, drng, 150, "150 bytes")
if actual3 != pythonDRNG150BytesExpected {
t.Errorf(
"Test vector 3 failed. Expected:\n%s\n\nGot:\n%s",
pythonDRNG150BytesExpected,
actual3,
)
}
// Test with fresh DRNG
drng2 := bip85.NewBIP85DRNG(entropy)
actual4 := readDRNGHex(t, drng2, 20, "20 bytes")
if actual4 != pythonDRNG20BytesExpected {
t.Errorf(
"Test vector 4 failed. Expected:\n%s\n\nGot:\n%s",
pythonDRNG20BytesExpected,
actual4,
)
}
// Read another 25 bytes
actual5 := readDRNGHex(t, drng2, 25, "25 bytes")
if actual5 != pythonDRNG25BytesExpected {
t.Errorf(
"Test vector 5 failed. Expected:\n%s\n\nGot:\n%s",
pythonDRNG25BytesExpected,
actual5,
)
}
}
// drngReadChunks performs sequential reads of the given sizes from drng
// and returns the concatenated output.
func drngReadChunks(t *testing.T, drng *bip85.DRNG, name string, sizes ...int) []byte {
t.Helper()
total := 0
for _, size := range sizes {
total += size
}
out := make([]byte, 0, total)
for _, size := range sizes {
buf := make([]byte, size)
_, err := drng.Read(buf)
if err != nil {
t.Fatalf("Failed to read from %s: %v", name, err)
}
out = append(out, buf...)
}
return out
}
// TestDRNGDeterminism tests the deterministic behavior of the DRNG
func TestDRNGDeterminism(t *testing.T) {
t.Parallel()
logTestVector(t, "DRNG Determinism")
masterKey := mustParseTestMasterKey(t)
// Derive entropy for the DRNG
entropy, err := bip85.DeriveBIP85Entropy(masterKey, drngTestPath)
if err != nil {
t.Fatalf("Failed to derive entropy: %v", err)
}
// Create 3 DRNGs with the same seed
drng1 := bip85.NewBIP85DRNG(entropy)
drng2 := bip85.NewBIP85DRNG(entropy)
drng3 := bip85.NewBIP85DRNG(entropy)
// Read the same amount of data with differently sized read calls
result1 := drngReadChunks(t, drng1, "drng1", 10, 20, 30, 40)
result2 := drngReadChunks(t, drng2, "drng2", 40, 30, 20, 10)
buf3 := drngReadChunks(t, drng3, "drng3", 100)
// All results should be identical
if !bytes.Equal(result1, result2) {
t.Errorf("Expected drng1 and drng2 to produce identical outputs")
}
if !bytes.Equal(result2, buf3) {
t.Errorf("Expected drng2 and drng3 to produce identical outputs")
}
}
// TestDRNGLengths tests the DRNG with different lengths
func TestDRNGLengths(t *testing.T) {
t.Parallel()
logTestVector(t, "DRNG Lengths")
masterKey := mustParseTestMasterKey(t)
// Derive entropy for the DRNG
entropy, err := bip85.DeriveBIP85Entropy(masterKey, drngTestPath)
if err != nil {
t.Fatalf("Failed to derive entropy: %v", err)
}
// Create DRNG
drng := bip85.NewBIP85DRNG(entropy)
// Test various lengths
lengths := []int{1, 10, 100, 1000, 10000}
for _, length := range lengths {
buffer := make([]byte, length)
n, err := drng.Read(buffer)
if err != nil {
t.Errorf("Failed to read %d bytes: %v", length, err)
continue
}
if n != length {
t.Errorf("Expected to read %d bytes, got %d", length, n)
}
}
}
// TestDRNGExceptions tests error handling in the DRNG
func TestDRNGExceptions(t *testing.T) {
t.Parallel()
logTestVector(t, "DRNG Exceptions")
// Test with entropy of the wrong size
testCases := []int{0, 1, 32, 63, 65, 128}
for _, size := range testCases {
t.Run(fmt.Sprintf("EntropySize_%d", size), func(t *testing.T) {
t.Parallel()
entropy := make([]byte, size)
// Use a function to capture the panic
testPanic := func() {
defer func() {
if r := recover(); r != nil {
// Expected behavior - panic occurred
return
}
}()
// This should panic for any size != 64
_ = bip85.NewBIP85DRNG(entropy)
// If we get here without panic, it's an error
t.Errorf(
"Expected panic for entropy length %d, but it didn't happen",
size,
)
}
testPanic()
})
}
}
// TestDRNGDifferentSizes tests the DRNG with different buffer sizes
func TestDRNGDifferentSizes(t *testing.T) {
t.Parallel()
logTestVector(t, "DRNG Different Sizes")
masterKey := mustParseTestMasterKey(t)
entropy, err := bip85.DeriveBIP85Entropy(masterKey, drngTestPath)
if err != nil {
t.Fatalf("Failed to derive entropy: %v", err)
}
// Create DRNG
drng := bip85.NewBIP85DRNG(entropy)
// Test reading different sizes
for _, size := range []int{32, 64, 128, 256} {
buffer := make([]byte, size)
n, err := drng.Read(buffer)
if err != nil {
t.Fatalf("Failed to read %d bytes from DRNG: %v", size, err)
}
if n != size {
t.Errorf("Expected to read %d bytes, got %d", size, n)
}
}
// Test deterministic behavior - two DRNGs with the same seed should
// produce the same output
drng1 := bip85.NewBIP85DRNG(entropy)
drng2 := bip85.NewBIP85DRNG(entropy)
buffer1 := make([]byte, 32)
buffer2 := make([]byte, 32)
_, err = drng1.Read(buffer1)
if err != nil {
t.Fatalf("Failed to read from first DRNG: %v", err)
}
_, err = drng2.Read(buffer2)
if err != nil {
t.Fatalf("Failed to read from second DRNG: %v", err)
}
if !bytes.Equal(buffer1, buffer2) {
t.Errorf("Expected identical outputs from DRNGs with same seed")
}
// Reading another 32 bytes should produce different output from the
// first read
buffer3 := make([]byte, 32)
_, err = drng1.Read(buffer3)
if err != nil {
t.Fatalf("Failed to read second buffer from DRNG: %v", err)
}
if bytes.Equal(buffer1, buffer3) {
t.Errorf("Expected different outputs from sequential reads")
}
}
// TestMasterKeyParsing tests parsing of different master key formats
func TestMasterKeyParsing(t *testing.T) {
t.Parallel()
logTestVector(t, "Master Key Parsing")
// Test valid master key
t.Logf("Testing valid master key")
_, err := bip85.ParseMasterKey(testMasterKey)
if err != nil {
t.Errorf("Failed to parse valid master key: %v", err)
} else {
t.Logf("Valid master key parsed successfully: PASS")
}
// Test invalid master key (wrong checksum)
t.Logf("Testing invalid master key (corrupted)")
_, err = bip85.ParseMasterKey(testInvalidMasterKey)
if err == nil {
t.Errorf("Expected error for invalid master key, but got nil")
} else {
t.Logf("Got expected error for invalid master key: %v", err)
t.Logf("RESULT: PASS")
}
// Test testnet master key (tprv)
t.Logf("Testing testnet master key format")
testnetMasterKey, err := bip85.ParseMasterKey(testTestnetMasterKey)
if err != nil {
t.Errorf("Failed to parse testnet master key: %v", err)
return
}
t.Logf("Testnet master key parsed successfully: PASS")
// Test that XPRV derivation using a testnet master key produces a
// testnet XPRV
derivedKey, err := bip85.DeriveXPRV(testnetMasterKey, 0)
if err != nil {
t.Fatalf("Failed to derive XPRV from testnet key: %v", err)
}
derivedKeyStr := derivedKey.String()
if !strings.HasPrefix(derivedKeyStr, "tprv") {
t.Errorf(
"Expected derived key to be testnet (tprv prefix), got: %s",
derivedKeyStr,
)
} else {
t.Logf("Testnet XPRV derived successfully: %s", derivedKeyStr)
t.Logf("RESULT: PASS")
}
}
// TestDifferentPathFormats tests different path format expressions
func TestDifferentPathFormats(t *testing.T) {
t.Parallel()
logTestVector(t, "Path Formats")
masterKey := mustParseTestMasterKey(t)
// Define equivalent paths in different formats
paths := []string{
"m/83696968'/0'/0'",
"m/83696968h/0h/0h",
"/83696968'/0'/0'",
"83696968'/0'/0'",
}
results := make([][]byte, 0, len(paths))
// Derive entropy using each path
for i, path := range paths {
t.Logf("Testing path format %d: %s", i+1, path)
entropy, err := bip85.DeriveBIP85Entropy(masterKey, path)
if err != nil {
t.Errorf("Failed to derive entropy with path %s: %v", path, err)
continue
}
results = append(results, entropy)
t.Logf("Derivation succeeded: PASS")
}
// Verify all results are the same
for i := 1; i < len(results); i++ {
if !bytes.Equal(results[0], results[i]) {
t.Errorf(
"Path %s produced different entropy than path %s",
paths[0],
paths[i],
)
}
}
if len(results) > 1 {
t.Logf("All equivalent path formats produced the same entropy: PASS")
}
}
// TestDirectBase85Encoding tests direct Base85 encoding with the test vector entropy
func TestDirectBase85Encoding(t *testing.T) {
t.Parallel()
logTestVector(t, "Direct Base85 Encoding")
// Parse the master key
masterKey := mustParseTestMasterKey(t)
// First, derive the entropy and verify it matches the test vector
derivedEntropy, err := bip85.DeriveBIP85Entropy(masterKey, pwdBase85Path)
if err != nil {
t.Fatalf("Failed to derive entropy: %v", err)
}
// This is the expected entropy from the BIP85 spec for the Base85
// test vector
expectedEntropy, err := hex.DecodeString(pwdBase85ExpectedEntropy)
if err != nil {
t.Fatalf("Failed to decode expected entropy hex: %v", err)
}
// Verify the derived entropy matches the expected entropy
derivedEntropyHex := hex.EncodeToString(derivedEntropy)
if derivedEntropyHex != pwdBase85ExpectedEntropy {
t.Errorf(
"Entropy mismatch!\nExpected: %s\nGot: %s",
pwdBase85ExpectedEntropy,
derivedEntropyHex,
)
}
// Verify the entropy bytes match
if !bytes.Equal(derivedEntropy, expectedEntropy) {
t.Errorf("Entropy bytes do not match the test vector")
}
// Now test the password generation
pwd, err := bip85.DeriveBase85Password(masterKey, 12, 0)
if err != nil {
t.Fatalf("Failed to derive Base85 password: %v", err)
}
// Expected password from the test vector
if pwd != pwdBase85ExpectedPassword {
t.Errorf(
"Password mismatch!\nExpected: '%s'\nGot: '%s'",
pwdBase85ExpectedPassword,
pwd,
)
}
}
// runPasswordVectorTest checks a BIP85 password derivation test vector.
func runPasswordVectorTest(
t *testing.T,
title, path string,
pwdLen uint32,
derive func(*hdkeychain.ExtendedKey, uint32, uint32) (string, error),
expectedEntropy, expectedPassword string,
) {
t.Helper()
logTestVector(t, title)
masterKey := mustParseTestMasterKey(t)
// Testing with the example from the BIP85 spec
t.Logf("Path: %s", path)
t.Logf("Parameters: Length=%d, Index=0", pwdLen)
// First verify the entropy derivation
entropy, err := bip85.DeriveBIP85Entropy(masterKey, path)
if err != nil {
t.Fatalf("Failed to derive entropy: %v", err)
}
// Expected entropy from BIP85 spec
derivedEntropyHex := hex.EncodeToString(entropy)
if derivedEntropyHex != expectedEntropy {
t.Errorf(
"Entropy mismatch!\nExpected: %s\nGot: %s",
expectedEntropy,
derivedEntropyHex,
)
}
// Now test the password generation
pwd, err := derive(masterKey, pwdLen, 0)
if err != nil {
t.Fatalf("Failed to derive password: %v", err)
}
// The test vector from the BIP85 specification
t.Logf("EXPECTED PASSWORD: %s", expectedPassword)
t.Logf("ACTUAL PASSWORD: %s", pwd)
if pwd != expectedPassword {
t.Errorf("Expected password '%s', got '%s'", expectedPassword, pwd)
} else {
t.Logf("RESULT: PASS")
}
t.Logf("Password length: %d characters", len(pwd))
}
// TestPWDBase64 tests the Base64 password test vector
func TestPWDBase64(t *testing.T) {
t.Parallel()
runPasswordVectorTest(
t,
"PWD Base64",
pwdBase64Path,
21,
bip85.DeriveBase64Password,
pwdBase64ExpectedEntropy,
pwdBase64ExpectedPassword,
)
}
// TestPWDBase85 tests the Base85 password test vector
func TestPWDBase85(t *testing.T) {
t.Parallel()
runPasswordVectorTest(
t,
"PWD Base85",
pwdBase85Path,
12,
bip85.DeriveBase85Password,
pwdBase85ExpectedEntropy,
pwdBase85ExpectedPassword,
)
}
// TestHexDerivation tests the HEX derivation test vector
func TestHexDerivation(t *testing.T) {
t.Parallel()
logTestVector(t, "HEX Derivation")
masterKey := mustParseTestMasterKey(t)
// Test vector from BIP85 spec
t.Logf("Path: %s", hexPath)
t.Logf("Parameters: NumBytes=64, Index=0")
// First verify the entropy derivation
entropy, err := bip85.DeriveBIP85Entropy(masterKey, hexPath)
if err != nil {
t.Fatalf("Failed to derive entropy: %v", err)
}
// Expected entropy from BIP85 spec; HEX uses first 64 bytes
derivedEntropyHex := hex.EncodeToString(entropy[:64])
if derivedEntropyHex != hexExpectedEntropy {
t.Errorf(
"Entropy mismatch!\nExpected: %s\nGot: %s",
hexExpectedEntropy,
derivedEntropyHex,
)
}
// Now test the hex derivation
hexData, err := bip85.DeriveHex(masterKey, 64, 0)
if err != nil {
t.Fatalf("Failed to derive hex data: %v", err)
}
if hexData != hexExpectedEntropy {
t.Errorf(
"Hex data mismatch!\nExpected: %s\nGot: %s",
hexExpectedEntropy,
hexData,
)
}
}
// TestInvalidParameters tests error conditions for parameter validation
func TestInvalidParameters(t *testing.T) {
t.Parallel()
logTestVector(t, "Invalid Parameters")
masterKey := mustParseTestMasterKey(t)
// Test cases for parameter validation
testCases := []struct {
name string
testFunc func() error
}{
{
name: "BIP39 invalid word count",
testFunc: func() error {
// 13 is not valid (must be 12, 15, 18, 21, 24)
_, err := bip85.DeriveBIP39Entropy(masterKey, 0, 13, 0)
return err
},
},
{
name: "Base64 password too short",
testFunc: func() error {
// Min is 20
_, err := bip85.DeriveBase64Password(masterKey, 19, 0)
return err
},
},
{
name: "Base64 password too long",
testFunc: func() error {
// Max is 86
_, err := bip85.DeriveBase64Password(masterKey, 87, 0)
return err
},
},
{
name: "Base85 password too short",
testFunc: func() error {
// Min is 10
_, err := bip85.DeriveBase85Password(masterKey, 9, 0)
return err
},
},
{
name: "Base85 password too long",
testFunc: func() error {
// Max is 80
_, err := bip85.DeriveBase85Password(masterKey, 81, 0)
return err
},
},
{
name: "Hex data too small",
testFunc: func() error {
// Min is 16
_, err := bip85.DeriveHex(masterKey, 15, 0)
return err
},
},
{
name: "Hex data too large",
testFunc: func() error {
// Max is 64
_, err := bip85.DeriveHex(masterKey, 65, 0)
return err
},
},
}
// Run all validation test cases
for _, tc := range testCases {
t.Logf("Testing: %s", tc.name)
err := tc.testFunc()
if err == nil {
t.Errorf("Expected error for %s, but got nil", tc.name)
} else {
t.Logf("Got expected error: %v", err)
t.Logf("RESULT: PASS")
}
}
}
// TestAdditionalDeriveHex tests additional hex derivation scenarios
func TestAdditionalDeriveHex(t *testing.T) {
t.Parallel()
logTestVector(t, "Additional Hex Derivation")
masterKey := mustParseTestMasterKey(t)
// Test min size (16 bytes)
hexMinBytes, err := bip85.DeriveHex(masterKey, 16, 0)
if err != nil {
t.Fatalf("Failed to derive 16-byte hex: %v", err)
}
t.Logf("16-byte hex: %s", hexMinBytes)
if len(hexMinBytes) != 32 { // 16 bytes = 32 hex chars
t.Errorf(
"Expected 32 hex chars (16 bytes), got %d chars",
len(hexMinBytes),
)
} else {
t.Logf("RESULT: PASS")
}
// Test max size (64 bytes)
hexMaxBytes, err := bip85.DeriveHex(masterKey, 64, 0)
if err != nil {
t.Fatalf("Failed to derive 64-byte hex: %v", err)
}
t.Logf("64-byte hex: %s", hexMaxBytes)
if len(hexMaxBytes) != 128 { // 64 bytes = 128 hex chars
t.Errorf(
"Expected 128 hex chars (64 bytes), got %d chars",
len(hexMaxBytes),
)
} else {
t.Logf("RESULT: PASS")
}
// Test different index values
hex1, err := bip85.DeriveHex(masterKey, 32, 0)
if err != nil {
t.Fatalf("Failed to derive hex with index 0: %v", err)
}
hex2, err := bip85.DeriveHex(masterKey, 32, 1)
if err != nil {
t.Fatalf("Failed to derive hex with index 1: %v", err)
}
t.Logf("Hex index 0: %s", hex1)
t.Logf("Hex index 1: %s", hex2)
if hex1 == hex2 {
t.Errorf("Expected different hex values for different indexes")
} else {
t.Logf("Different indexes produced different outputs: PASS")
}
}