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

206
internal/agehd/README.md Normal file
View File

@ -0,0 +1,206 @@
# agehd - Deterministic Age Identities from BIP85
The `agehd` package derives deterministic X25519 age identities using BIP85 entropy derivation and a deterministic random number generator (DRNG). This package only supports proper BIP85 sources: BIP39 mnemonics and extended private keys (xprv).
## Features
- **Deterministic key generation**: Same input always produces the same age identity
- **BIP85 compliance**: Uses the BIP85 standard for entropy derivation
- **Multiple key support**: Generate multiple keys from the same source using different indices
- **Two BIP85 input methods**: Support for BIP39 mnemonics and extended private keys (xprv)
- **Vendor/application scoped**: Uses vendor-specific derivation paths to avoid conflicts
## Derivation Path
The package uses the following BIP85 derivation path:
```
m/83696968'/592366788'/733482323'/n'
```
Where:
- `83696968'` is the BIP85 root path ("bip" in ASCII)
- `592366788'` is the vendor ID (sha256("berlin.sneak") & 0x7fffffff)
- `733482323'` is the application ID (sha256("secret") & 0x7fffffff)
- `n'` is the sequential index (0, 1, 2, ...)
## Usage
### From BIP39 Mnemonic
```go
package main
import (
"fmt"
"log"
"git.eeqj.de/sneak/secret/internal/agehd"
)
func main() {
mnemonic := "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
// Derive the first identity (index 0)
identity, err := agehd.DeriveIdentity(mnemonic, 0)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Secret key: %s\n", identity.String())
fmt.Printf("Public key: %s\n", identity.Recipient().String())
}
```
### From Extended Private Key (XPRV)
```go
package main
import (
"fmt"
"log"
"git.eeqj.de/sneak/secret/internal/agehd"
)
func main() {
xprv := "xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLjbKb"
// Derive the first identity (index 0) from the xprv
identity, err := agehd.DeriveIdentityFromXPRV(xprv, 0)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Secret key: %s\n", identity.String())
fmt.Printf("Public key: %s\n", identity.Recipient().String())
}
```
### Multiple Identities
```go
package main
import (
"fmt"
"log"
"git.eeqj.de/sneak/secret/internal/agehd"
)
func main() {
mnemonic := "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
// Derive multiple identities with different indices
for i := uint32(0); i < 3; i++ {
identity, err := agehd.DeriveIdentity(mnemonic, i)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Identity %d: %s\n", i, identity.Recipient().String())
}
}
```
### From Raw Entropy
```go
package main
import (
"fmt"
"log"
"git.eeqj.de/sneak/secret/internal/agehd"
)
func main() {
mnemonic := "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
// First derive entropy using BIP85
entropy, err := agehd.DeriveEntropy(mnemonic, 0)
if err != nil {
log.Fatal(err)
}
// Then create identity from entropy
identity, err := agehd.IdentityFromEntropy(entropy)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Secret key: %s\n", identity.String())
fmt.Printf("Public key: %s\n", identity.Recipient().String())
}
```
## API Reference
### Functions
#### `DeriveIdentity(mnemonic string, n uint32) (*age.X25519Identity, error)`
Derives a deterministic age identity from a BIP39 mnemonic and index.
- `mnemonic`: A valid BIP39 mnemonic phrase
- `n`: The derivation index (0, 1, 2, ...)
- Returns: An age X25519 identity or an error
#### `DeriveIdentityFromXPRV(xprv string, n uint32) (*age.X25519Identity, error)`
Derives a deterministic age identity from an extended private key (xprv) and index.
- `xprv`: A valid extended private key in xprv format
- `n`: The derivation index (0, 1, 2, ...)
- Returns: An age X25519 identity or an error
#### `DeriveEntropy(mnemonic string, n uint32) ([]byte, error)`
Derives 32 bytes of entropy from a BIP39 mnemonic and index using BIP85.
- `mnemonic`: A valid BIP39 mnemonic phrase
- `n`: The derivation index
- Returns: 32 bytes of entropy or an error
#### `DeriveEntropyFromXPRV(xprv string, n uint32) ([]byte, error)`
Derives 32 bytes of entropy from an extended private key (xprv) and index using BIP85.
- `xprv`: A valid extended private key in xprv format
- `n`: The derivation index
- Returns: 32 bytes of entropy or an error
#### `IdentityFromEntropy(ent []byte) (*age.X25519Identity, error)`
Converts 32 bytes of entropy into an age X25519 identity.
- `ent`: Exactly 32 bytes of entropy
- Returns: An age X25519 identity or an error
## Implementation Details
1. **BIP85 Entropy Derivation**: The package uses the BIP85 standard to derive 64 bytes of entropy from the input source
2. **DRNG**: A BIP85 DRNG (Deterministic Random Number Generator) using SHAKE256 is seeded with the 64-byte entropy
3. **Key Generation**: 32 bytes are read from the DRNG to generate the age private key
4. **RFC-7748 Clamping**: The private key is clamped according to RFC-7748 for X25519
5. **Bech32 Encoding**: The key is encoded using Bech32 with the "age-secret-key-" prefix
## Security Considerations
- The same mnemonic/xprv and index will always produce the same identity
- Different indices produce cryptographically independent identities
- The vendor/application scoping prevents conflicts with other BIP85 applications
- The DRNG ensures high-quality randomness for key generation
- Private keys are properly clamped for X25519 usage
- Only accepts proper BIP85 sources (mnemonics and xprv keys), not arbitrary passphrases
## Testing
Run the tests with:
```bash
go test -v ./internal/agehd
```

View File

@ -13,17 +13,17 @@ import (
"strings"
"filippo.io/age"
bip85 "github.com/bitmask-dev/go-bip85"
"git.eeqj.de/sneak/secret/internal/bip85"
"github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcutil/bech32"
"github.com/tyler-smith/go-bip32"
"github.com/tyler-smith/go-bip39"
)
const (
purpose = uint32(83696968) // fixed by BIP-85 (“bip”)
purpose = uint32(83696968) // fixed by BIP-85 ("bip")
vendorID = uint32(592366788) // berlin.sneak
appID = uint32(733482323) // secret
firstH = bip32.FirstHardenedChild
hrp = "age-secret-key-" // Bech32 HRP used by age
)
@ -40,9 +40,13 @@ func IdentityFromEntropy(ent []byte) (*age.X25519Identity, error) {
if len(ent) != 32 {
return nil, fmt.Errorf("need 32-byte scalar, got %d", len(ent))
}
clamp(ent)
data, err := bech32.ConvertBits(ent, 8, 5, true)
// Make a copy to avoid modifying the original
key := make([]byte, 32)
copy(key, ent)
clamp(key)
data, err := bech32.ConvertBits(key, 8, 5, true)
if err != nil {
return nil, fmt.Errorf("bech32 convert: %w", err)
}
@ -54,29 +58,68 @@ func IdentityFromEntropy(ent []byte) (*age.X25519Identity, error) {
}
// DeriveEntropy derives 32 bytes of application-scoped entropy from the
// supplied BIP-39 mnemonic and index n.
// supplied BIP-39 mnemonic and index n using BIP85.
func DeriveEntropy(mnemonic string, n uint32) ([]byte, error) {
// Convert mnemonic to seed
seed := bip39.NewSeed(mnemonic, "")
root, err := bip32.NewMasterKey(seed)
// Create master key from seed
masterKey, err := hdkeychain.NewMaster(seed, &chaincfg.MainNetParams)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to create master key: %w", err)
}
// m / purpose / vendor
purp, err := root.NewChildKey(purpose + firstH)
// Build the BIP85 derivation path: m/83696968'/vendor'/app'/n'
path := fmt.Sprintf("m/%d'/%d'/%d'/%d'", purpose, vendorID, appID, n)
// Derive BIP85 entropy (64 bytes)
entropy, err := bip85.DeriveBIP85Entropy(masterKey, path)
if err != nil {
return nil, err
}
vend, err := purp.NewChildKey(vendorID + firstH)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to derive BIP85 entropy: %w", err)
}
return bip85.NewFromBip32(vend).DeriveRawEntropy(appID, 32, n)
// Use BIP85 DRNG to generate deterministic 32 bytes for the age key
drng := bip85.NewBIP85DRNG(entropy)
key := make([]byte, 32)
_, err = drng.Read(key)
if err != nil {
return nil, fmt.Errorf("failed to read from DRNG: %w", err)
}
return key, nil
}
// DeriveIdentity is the primary public helper.
// DeriveEntropyFromXPRV derives 32 bytes of application-scoped entropy from the
// supplied extended private key (xprv) and index n using BIP85.
func DeriveEntropyFromXPRV(xprv string, n uint32) ([]byte, error) {
// Parse the extended private key
masterKey, err := bip85.ParseMasterKey(xprv)
if err != nil {
return nil, fmt.Errorf("failed to parse master key: %w", err)
}
// Build the BIP85 derivation path: m/83696968'/vendor'/app'/n'
path := fmt.Sprintf("m/%d'/%d'/%d'/%d'", purpose, vendorID, appID, n)
// Derive BIP85 entropy (64 bytes)
entropy, err := bip85.DeriveBIP85Entropy(masterKey, path)
if err != nil {
return nil, fmt.Errorf("failed to derive BIP85 entropy: %w", err)
}
// Use BIP85 DRNG to generate deterministic 32 bytes for the age key
drng := bip85.NewBIP85DRNG(entropy)
key := make([]byte, 32)
_, err = drng.Read(key)
if err != nil {
return nil, fmt.Errorf("failed to read from DRNG: %w", err)
}
return key, nil
}
// DeriveIdentity is the primary public helper that derives a deterministic
// age identity from a BIP39 mnemonic and index.
func DeriveIdentity(mnemonic string, n uint32) (*age.X25519Identity, error) {
ent, err := DeriveEntropy(mnemonic, n)
if err != nil {
@ -84,3 +127,13 @@ func DeriveIdentity(mnemonic string, n uint32) (*age.X25519Identity, error) {
}
return IdentityFromEntropy(ent)
}
// DeriveIdentityFromXPRV derives a deterministic age identity from an
// extended private key (xprv) and index.
func DeriveIdentityFromXPRV(xprv string, n uint32) (*age.X25519Identity, error) {
ent, err := DeriveEntropyFromXPRV(xprv, n)
if err != nil {
return nil, err
}
return IdentityFromEntropy(ent)
}

View File

@ -8,8 +8,13 @@ import (
"filippo.io/age"
)
const mnemonic = "abandon abandon abandon abandon abandon " +
"abandon abandon abandon abandon abandon abandon about"
const (
mnemonic = "abandon abandon abandon abandon abandon " +
"abandon abandon abandon abandon abandon abandon about"
// Test xprv from BIP85 test vectors
testXPRV = "xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLjbKb"
)
func TestEncryptDecrypt(t *testing.T) {
id, err := DeriveIdentity(mnemonic, 0)
@ -45,3 +50,174 @@ func TestEncryptDecrypt(t *testing.T) {
t.Fatalf("round-trip mismatch: %q", got)
}
}
func TestDeriveIdentityFromXPRV(t *testing.T) {
id, err := DeriveIdentityFromXPRV(testXPRV, 0)
if err != nil {
t.Fatalf("derive from xprv: %v", err)
}
t.Logf("xprv secret: %s", id.String())
t.Logf("xprv recipient: %s", id.Recipient().String())
// Test encryption/decryption with xprv-derived identity
var ct bytes.Buffer
w, err := age.Encrypt(&ct, id.Recipient())
if err != nil {
t.Fatalf("encrypt init: %v", err)
}
if _, err = io.WriteString(w, "hello from xprv"); err != nil {
t.Fatalf("write: %v", err)
}
if err = w.Close(); err != nil {
t.Fatalf("encrypt close: %v", err)
}
r, err := age.Decrypt(bytes.NewReader(ct.Bytes()), id)
if err != nil {
t.Fatalf("decrypt init: %v", err)
}
dec, err := io.ReadAll(r)
if err != nil {
t.Fatalf("read: %v", err)
}
if got := string(dec); got != "hello from xprv" {
t.Fatalf("round-trip mismatch: %q", got)
}
}
func TestDeterministicDerivation(t *testing.T) {
// Test that the same mnemonic and index always produce the same identity
id1, err := DeriveIdentity(mnemonic, 0)
if err != nil {
t.Fatalf("derive 1: %v", err)
}
id2, err := DeriveIdentity(mnemonic, 0)
if err != nil {
t.Fatalf("derive 2: %v", err)
}
if id1.String() != id2.String() {
t.Fatalf("identities should be deterministic: %s != %s", id1.String(), id2.String())
}
// Test that different indices produce different identities
id3, err := DeriveIdentity(mnemonic, 1)
if err != nil {
t.Fatalf("derive 3: %v", err)
}
if id1.String() == id3.String() {
t.Fatalf("different indices should produce different identities")
}
t.Logf("Index 0: %s", id1.String())
t.Logf("Index 1: %s", id3.String())
}
func TestDeterministicXPRVDerivation(t *testing.T) {
// Test that the same xprv and index always produce the same identity
id1, err := DeriveIdentityFromXPRV(testXPRV, 0)
if err != nil {
t.Fatalf("derive 1: %v", err)
}
id2, err := DeriveIdentityFromXPRV(testXPRV, 0)
if err != nil {
t.Fatalf("derive 2: %v", err)
}
if id1.String() != id2.String() {
t.Fatalf("xprv identities should be deterministic: %s != %s", id1.String(), id2.String())
}
// Test that different indices with same xprv produce different identities
id3, err := DeriveIdentityFromXPRV(testXPRV, 1)
if err != nil {
t.Fatalf("derive 3: %v", err)
}
if id1.String() == id3.String() {
t.Fatalf("different indices should produce different identities")
}
t.Logf("XPRV Index 0: %s", id1.String())
t.Logf("XPRV Index 1: %s", id3.String())
}
func TestMnemonicVsXPRVConsistency(t *testing.T) {
// Test that deriving from mnemonic and from the corresponding xprv produces the same result
// Note: This test is removed because the test mnemonic and test xprv are from different sources
// and are not expected to produce the same results.
t.Skip("Skipping consistency test - test mnemonic and xprv are from different sources")
}
func TestEntropyLength(t *testing.T) {
// Test that DeriveEntropy returns exactly 32 bytes
entropy, err := DeriveEntropy(mnemonic, 0)
if err != nil {
t.Fatalf("derive entropy: %v", err)
}
if len(entropy) != 32 {
t.Fatalf("expected 32 bytes of entropy, got %d", len(entropy))
}
t.Logf("Entropy (32 bytes): %x", entropy)
// Test that DeriveEntropyFromXPRV returns exactly 32 bytes
entropyXPRV, err := DeriveEntropyFromXPRV(testXPRV, 0)
if err != nil {
t.Fatalf("derive entropy from xprv: %v", err)
}
if len(entropyXPRV) != 32 {
t.Fatalf("expected 32 bytes of entropy from xprv, got %d", len(entropyXPRV))
}
t.Logf("XPRV Entropy (32 bytes): %x", entropyXPRV)
// Note: We don't compare the entropy values since the test mnemonic and test xprv
// are from different sources and should produce different entropy values.
}
func TestIdentityFromEntropy(t *testing.T) {
// Test that IdentityFromEntropy works with custom entropy
entropy := make([]byte, 32)
for i := range entropy {
entropy[i] = byte(i)
}
id, err := IdentityFromEntropy(entropy)
if err != nil {
t.Fatalf("identity from entropy: %v", err)
}
t.Logf("Custom entropy identity: %s", id.String())
// Test that it rejects wrong-sized entropy
_, err = IdentityFromEntropy(entropy[:31])
if err == nil {
t.Fatalf("expected error for 31-byte entropy")
}
// Create a 33-byte slice to test rejection
entropy33 := make([]byte, 33)
copy(entropy33, entropy)
_, err = IdentityFromEntropy(entropy33)
if err == nil {
t.Fatalf("expected error for 33-byte entropy")
}
}
func TestInvalidXPRV(t *testing.T) {
// Test with invalid xprv
_, err := DeriveIdentityFromXPRV("invalid-xprv", 0)
if err == nil {
t.Fatalf("expected error for invalid xprv")
}
t.Logf("Got expected error for invalid xprv: %v", err)
}

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