latest
This commit is contained in:
@@ -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)
|
||||
@@ -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
Reference in New Issue
Block a user