package bip85 import ( "bytes" "crypto/hmac" "crypto/sha256" "crypto/sha512" "encoding/base64" "encoding/binary" "encoding/hex" "fmt" "io" "strings" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil/base58" "github.com/btcsuite/btcd/btcutil/hdkeychain" "github.com/btcsuite/btcd/chaincfg" "golang.org/x/crypto/sha3" ) const ( // BIP85_MASTER_PATH is the derivation path prefix for all BIP85 applications BIP85_MASTER_PATH = "m/83696968'" // BIP85_KEY_HMAC_KEY is the HMAC key used for deriving the entropy BIP85_KEY_HMAC_KEY = "bip-entropy-from-k" // Application numbers APP_BIP39 = 39 // BIP39 mnemonics APP_HD_WIF = 2 // WIF for Bitcoin Core APP_XPRV = 32 // Extended private key APP_HEX = 128169 APP_PWD64 = 707764 // Base64 passwords APP_PWD85 = 707785 // Base85 passwords APP_RSA = 828365 ) // Version bytes for extended keys var ( // MainNetPrivateKey is the version for mainnet private keys MainNetPrivateKey = []byte{0x04, 0x88, 0xAD, 0xE4} // TestNetPrivateKey is the version for testnet private keys TestNetPrivateKey = []byte{0x04, 0x35, 0x83, 0x94} ) // BIP85DRNG is a deterministic random number generator seeded by BIP85 entropy type BIP85DRNG struct { shake io.Reader } // NewBIP85DRNG creates a new DRNG seeded with BIP85 entropy func NewBIP85DRNG(entropy []byte) *BIP85DRNG { // The entropy must be exactly 64 bytes (512 bits) if len(entropy) != 64 { panic("BIP85DRNG entropy must be 64 bytes") } // Initialize SHAKE256 with the entropy shake := sha3.NewShake256() shake.Write(entropy) return &BIP85DRNG{ shake: shake, } } // Read implements the io.Reader interface func (d *BIP85DRNG) Read(p []byte) (n int, err error) { return d.shake.Read(p) } // DeriveChildKey returns the private key and chain code bytes func DeriveChildKey(masterKey *hdkeychain.ExtendedKey, path string) ([]byte, error) { // Validate the masterKey is a private key if !masterKey.IsPrivate() { return nil, fmt.Errorf("master key must be a private key") } // Derive the child key at the specified path childKey, err := deriveChildKey(masterKey, path) if err != nil { return nil, fmt.Errorf("failed to derive child key: %w", err) } // Get the private key bytes ecPrivKey, err := childKey.ECPrivKey() if err != nil { return nil, fmt.Errorf("failed to get EC private key: %w", err) } // Serialize the private key to get the bytes return ecPrivKey.Serialize(), nil } // DeriveBIP85Entropy derives entropy from a BIP32 master key using the BIP85 method func DeriveBIP85Entropy(masterKey *hdkeychain.ExtendedKey, path string) ([]byte, error) { // Get the child key bytes privKeyBytes, err := DeriveChildKey(masterKey, path) if err != nil { return nil, err } // Apply HMAC-SHA512 h := hmac.New(sha512.New, []byte(BIP85_KEY_HMAC_KEY)) h.Write(privKeyBytes) entropy := h.Sum(nil) return entropy, nil } // deriveChildKey derives a child key from a parent key using the given path func deriveChildKey(parent *hdkeychain.ExtendedKey, path string) (*hdkeychain.ExtendedKey, error) { if path == "" || path == "m" || path == "/" { return parent, nil } // Remove the "m/" or "/" prefix if present path = strings.TrimPrefix(path, "m/") path = strings.TrimPrefix(path, "/") // Split the path into individual components components := strings.Split(path, "/") // Start with the parent key key := parent // Derive each component for _, component := range components { // Check if the component is hardened hardened := strings.HasSuffix(component, "'") || strings.HasSuffix(component, "h") if hardened { component = strings.TrimSuffix(component, "'") component = strings.TrimSuffix(component, "h") } // Parse the index var index uint32 _, err := fmt.Sscanf(component, "%d", &index) if err != nil { return nil, fmt.Errorf("invalid path component: %s", component) } // Apply hardening if needed if hardened { index += hdkeychain.HardenedKeyStart } // Derive the child key child, err := key.Derive(index) if err != nil { return nil, fmt.Errorf("failed to derive child key at index %d: %w", index, err) } key = child } return key, nil } // DeriveBIP39Entropy derives entropy for a BIP39 mnemonic func DeriveBIP39Entropy(masterKey *hdkeychain.ExtendedKey, language, words, index uint32) ([]byte, error) { path := fmt.Sprintf("%s/%d'/%d'/%d'/%d'", BIP85_MASTER_PATH, APP_BIP39, language, words, index) entropy, err := DeriveBIP85Entropy(masterKey, path) if err != nil { return nil, err } // Determine how many bits of entropy to use based on the words var bits int switch words { case 12: bits = 128 case 15: bits = 160 case 18: bits = 192 case 21: bits = 224 case 24: bits = 256 default: return nil, fmt.Errorf("invalid BIP39 word count: %d", words) } // Truncate to the required number of bits (bytes = bits / 8) entropy = entropy[:bits/8] return entropy, nil } // DeriveWIFKey derives a private key in WIF format func DeriveWIFKey(masterKey *hdkeychain.ExtendedKey, index uint32) (string, error) { path := fmt.Sprintf("%s/%d'/%d'", BIP85_MASTER_PATH, APP_HD_WIF, index) entropy, err := DeriveBIP85Entropy(masterKey, path) if err != nil { return "", err } // Use the first 32 bytes as the key keyBytes := entropy[:32] // Convert to WIF format privKey, _ := btcec.PrivKeyFromBytes(keyBytes) wif, err := btcutil.NewWIF(privKey, &chaincfg.MainNetParams, true) // compressed=true if err != nil { return "", fmt.Errorf("failed to create WIF: %w", err) } return wif.String(), nil } // DeriveXPRV derives an extended private key (XPRV) func DeriveXPRV(masterKey *hdkeychain.ExtendedKey, index uint32) (*hdkeychain.ExtendedKey, error) { path := fmt.Sprintf("%s/%d'/%d'", BIP85_MASTER_PATH, APP_XPRV, index) entropy, err := DeriveBIP85Entropy(masterKey, path) if err != nil { return nil, err } // The first 32 bytes are the chain code, the second 32 bytes are the private key chainCode := entropy[:32] privateKey := entropy[32:64] // Create serialized extended key var serialized bytes.Buffer // Add version bytes (4 bytes) // Default to mainnet version version := MainNetPrivateKey // Check if the master key serialization starts with the testnet version bytes masterKeyStr := masterKey.String() if strings.HasPrefix(masterKeyStr, "tprv") { version = TestNetPrivateKey } // Write serialized data serialized.Write(version) // 4 bytes: version serialized.WriteByte(0) // 1 byte: depth (0 for master) serialized.Write([]byte{0, 0, 0, 0}) // 4 bytes: parent fingerprint (0 for master) serialized.Write([]byte{0, 0, 0, 0}) // 4 bytes: child number (0 for master) serialized.Write(chainCode) // 32 bytes: chain code serialized.WriteByte(0) // 1 byte: 0x00 prefix for private key serialized.Write(privateKey) // 32 bytes: private key // Calculate checksum (first 4 bytes of double-SHA256) serializedBytes := serialized.Bytes() checksum := doubleSHA256(serializedBytes)[:4] // Append checksum serializedWithChecksum := append(serializedBytes, checksum...) // Base58 encode xprvStr := base58.Encode(serializedWithChecksum) // Parse the serialized xprv back to an ExtendedKey return hdkeychain.NewKeyFromString(xprvStr) } // doubleSHA256 calculates sha256(sha256(data)) func doubleSHA256(data []byte) []byte { hash1 := sha256.Sum256(data) hash2 := sha256.Sum256(hash1[:]) return hash2[:] } // DeriveHex derives a raw hex string of specified length func DeriveHex(masterKey *hdkeychain.ExtendedKey, numBytes, index uint32) (string, error) { if numBytes < 16 || numBytes > 64 { return "", fmt.Errorf("numBytes must be between 16 and 64") } path := fmt.Sprintf("%s/%d'/%d'/%d'", BIP85_MASTER_PATH, APP_HEX, numBytes, index) entropy, err := DeriveBIP85Entropy(masterKey, path) if err != nil { return "", err } // Truncate to the required number of bytes entropy = entropy[:numBytes] return hex.EncodeToString(entropy), nil } // DeriveBase64Password derives a password encoded in Base64 func DeriveBase64Password(masterKey *hdkeychain.ExtendedKey, pwdLen, index uint32) (string, error) { if pwdLen < 20 || pwdLen > 86 { return "", fmt.Errorf("pwdLen must be between 20 and 86") } path := fmt.Sprintf("%s/%d'/%d'/%d'", BIP85_MASTER_PATH, APP_PWD64, pwdLen, index) entropy, err := DeriveBIP85Entropy(masterKey, path) if err != nil { return "", err } // Base64 encode all 64 bytes of entropy encodedStr := base64.StdEncoding.EncodeToString(entropy) // Remove any padding encodedStr = strings.TrimRight(encodedStr, "=") // Slice to the desired password length if uint32(len(encodedStr)) < pwdLen { return "", fmt.Errorf("derived password length %d is shorter than requested length %d", len(encodedStr), pwdLen) } return encodedStr[:pwdLen], nil } // DeriveBase85Password derives a password encoded in Base85 func DeriveBase85Password(masterKey *hdkeychain.ExtendedKey, pwdLen, index uint32) (string, error) { if pwdLen < 10 || pwdLen > 80 { return "", fmt.Errorf("pwdLen must be between 10 and 80") } path := fmt.Sprintf("%s/%d'/%d'/%d'", BIP85_MASTER_PATH, APP_PWD85, pwdLen, index) entropy, err := DeriveBIP85Entropy(masterKey, path) if err != nil { return "", err } // Base85 encode all 64 bytes of entropy using the RFC1924 character set encoded := encodeBase85WithRFC1924Charset(entropy) // Slice to the desired password length if uint32(len(encoded)) < pwdLen { return "", fmt.Errorf("encoded length %d is less than requested length %d", len(encoded), pwdLen) } return encoded[:pwdLen], nil } // encodeBase85WithRFC1924Charset encodes data using Base85 with the RFC1924 character set func encodeBase85WithRFC1924Charset(data []byte) string { // RFC1924 character set charset := "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!#$%&()*+-;<=>?@^_`{|}~" // Pad data to multiple of 4 padded := make([]byte, ((len(data)+3)/4)*4) copy(padded, data) var buf strings.Builder buf.Grow(len(padded) * 5 / 4) // Each 4 bytes becomes 5 Base85 characters // Process in 4-byte chunks for i := 0; i < len(padded); i += 4 { // Convert 4 bytes to uint32 (big-endian) chunk := binary.BigEndian.Uint32(padded[i : i+4]) // Convert to 5 base-85 digits digits := make([]byte, 5) for j := 4; j >= 0; j-- { idx := chunk % 85 digits[j] = charset[idx] chunk /= 85 } buf.Write(digits) } return buf.String() } // ParseMasterKey parses an extended key from a string func ParseMasterKey(xprv string) (*hdkeychain.ExtendedKey, error) { return hdkeychain.NewKeyFromString(xprv) }