secret/pkg/bip85/bip85.go
2025-05-28 14:06:29 -07:00

377 lines
10 KiB
Go

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)
}