This commit is contained in:
2025-05-28 07:37:57 -07:00
parent 7671eaaa57
commit 6a8bd3388c
6 changed files with 1094 additions and 449 deletions

View File

@@ -8,7 +8,7 @@ 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 dice rolls, hex values, and passwords
- Generate deterministic random values for hex values and passwords
## Usage Examples
@@ -83,24 +83,6 @@ if err != nil {
fmt.Println("32 bytes of hex:", hex)
```
### Dice Rolls
```go
// Generate dice rolls
// sides: number of sides on the die
// rolls: number of rolls to generate
// index: allows multiple sets of the same type
rolls, err := bip85.DeriveDiceRolls(masterKey, 6, 10, 0)
if err != nil {
panic(err)
}
fmt.Print("10 rolls of a 6-sided die: ")
for _, roll := range rolls {
fmt.Print(roll, " ")
}
fmt.Println()
```
### DRNG (Deterministic Random Number Generator)
```go
@@ -140,7 +122,6 @@ Where:
- `128169'` for HEX data
- `707764'` for Base64 passwords
- `707785'` for Base85 passwords
- `89101'` for dice rolls
- `828365'` for RSA keys
- `{parameters}` are application-specific parameters
@@ -153,7 +134,8 @@ This implementation passes all the test vectors from the BIP85 specification:
- HD-WIF keys
- XPRV
- SHAKE256 DRNG output
- Dice rolls
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:
@@ -164,6 +146,7 @@ go test -v git.eeqj.de/sneak/secret/internal/bip85
## References
- [BIP85 Specification](https://github.com/bitcoin/bips/blob/master/bip-0085.mediawiki)
- [Python Reference Implementation](https://github.com/ethankosakovsky/bip85)
- [Bitcoin Core](https://github.com/bitcoin/bitcoin)
- [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki)
- [BIP39](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki)

View File

@@ -34,7 +34,6 @@ const (
APP_HEX = 128169
APP_PWD64 = 707764 // Base64 passwords
APP_PWD85 = 707785 // Base85 passwords
APP_DICE = 89101
APP_RSA = 828365
)
@@ -329,130 +328,46 @@ func DeriveBase85Password(masterKey *hdkeychain.ExtendedKey, pwdLen, index uint3
return "", err
}
// For the test vector specifically, match exactly what's in the spec
if pwdLen == 12 && index == 0 {
// This is the test vector from the BIP85 spec
return "_s`{TW89)i4`", nil
}
// ASCII85/Base85 encode the entropy
encodedStr := ascii85Encode(entropy)
// Base85 encode all 64 bytes of entropy using the RFC1924 character set
encoded := encodeBase85WithRFC1924Charset(entropy)
// 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)
if uint32(len(encoded)) < pwdLen {
return "", fmt.Errorf("encoded length %d is less than requested length %d", len(encoded), pwdLen)
}
return encodedStr[:pwdLen], nil
return encoded[:pwdLen], nil
}
// ascii85Encode encodes the data into a Base85/ASCII85 string
// This is a simple implementation that doesn't handle special cases
func ascii85Encode(data []byte) string {
// The maximum expansion of Base85 encoding is 5 characters for 4 input bytes
// For 64 bytes, that's potentially 80 characters
var result strings.Builder
result.Grow(80)
// encodeBase85WithRFC1924Charset encodes data using Base85 with the RFC1924 character set
func encodeBase85WithRFC1924Charset(data []byte) string {
// RFC1924 character set
charset := "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!#$%&()*+-;<=>?@^_`{|}~"
for i := 0; i < len(data); i += 4 {
// Process 4 bytes at a time
var value uint32
for j := 0; j < 4 && i+j < len(data); j++ {
value |= uint32(data[i+j]) << (24 - j*8)
}
// Pad data to multiple of 4
padded := make([]byte, ((len(data)+3)/4)*4)
copy(padded, data)
// Convert into 5 Base85 characters
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-- {
// Get the remainder when dividing by 85
remainder := value % 85
// Convert to ASCII range (33-117) and add to result
result.WriteByte(byte(remainder) + 33)
// Integer division by 85
value /= 85
}
}
return result.String()
}
// DeriveDiceRolls derives dice rolls according to the BIP85 specification
func DeriveDiceRolls(masterKey *hdkeychain.ExtendedKey, sides, rolls, index uint32) ([]uint32, error) {
if sides < 2 {
return nil, fmt.Errorf("sides must be at least 2")
}
if rolls < 1 {
return nil, fmt.Errorf("rolls must be at least 1")
}
path := fmt.Sprintf("%s/%d'/%d'/%d'/%d'", BIP85_MASTER_PATH, APP_DICE, sides, rolls, index)
entropy, err := DeriveBIP85Entropy(masterKey, path)
if err != nil {
return nil, err
}
// Create a DRNG
drng := NewBIP85DRNG(entropy)
// Calculate bits per roll
bitsPerRoll := calcBitsPerRoll(sides)
bytesPerRoll := (bitsPerRoll + 7) / 8
// The dice rolls test vector uses the following values:
// Sides: 6, Rolls: 10
if sides == 6 && rolls == 10 && index == 0 {
// Hard-coded values from the specification
return []uint32{1, 0, 0, 2, 0, 1, 5, 5, 2, 4}, nil
}
// Generate the rolls
result := make([]uint32, 0, rolls)
buffer := make([]byte, bytesPerRoll)
for uint32(len(result)) < rolls {
// Read bytes for a single roll
_, err := drng.Read(buffer)
if err != nil {
return nil, fmt.Errorf("failed to generate roll: %w", err)
idx := chunk % 85
digits[j] = charset[idx]
chunk /= 85
}
// Convert bytes to uint32
var roll uint32
switch bytesPerRoll {
case 1:
roll = uint32(buffer[0])
case 2:
roll = uint32(binary.BigEndian.Uint16(buffer))
case 3:
roll = (uint32(buffer[0]) << 16) | (uint32(buffer[1]) << 8) | uint32(buffer[2])
case 4:
roll = binary.BigEndian.Uint32(buffer)
}
// Mask extra bits
roll &= (1 << bitsPerRoll) - 1
// Check if roll is valid
if roll < sides {
result = append(result, roll)
}
// If roll >= sides, discard and generate a new one
buf.Write(digits)
}
return result, nil
}
// calcBitsPerRoll calculates the minimum number of bits needed to represent a die with 'sides' sides
func calcBitsPerRoll(sides uint32) uint {
bitsNeeded := uint(0)
maxValue := uint32(1)
for maxValue < sides {
bitsNeeded++
maxValue <<= 1
}
return bitsNeeded
return buf.String()
}
// ParseMasterKey parses an extended key from a string

File diff suppressed because it is too large Load Diff