This commit is contained in:
2025-05-28 14:06:29 -07:00
parent efedbe405f
commit 354681b298
14 changed files with 1749 additions and 239 deletions

206
pkg/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/pkg/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/pkg/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/pkg/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/pkg/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
```

139
pkg/agehd/agehd.go Normal file
View File

@@ -0,0 +1,139 @@
// Package agehd derives deterministic X25519 age identities from a
// BIP-39 seed using a vendor/application-scoped BIP-85 path:
//
// m / 83696968 / <vendor id> / <application id> / n
//
// • vendor id = 592 366 788 (sha256("berlin.sneak") & 0x7fffffff)
// • app id = 733 482 323 (sha256("secret") & 0x7fffffff)
// • n = sequential index (0,1,…)
package agehd
import (
"fmt"
"strings"
"filippo.io/age"
"git.eeqj.de/sneak/secret/pkg/bip85"
"github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcutil/bech32"
"github.com/tyler-smith/go-bip39"
)
const (
purpose = uint32(83696968) // fixed by BIP-85 ("bip")
vendorID = uint32(592366788) // berlin.sneak
appID = uint32(733482323) // secret
hrp = "age-secret-key-" // Bech32 HRP used by age
)
// clamp applies RFC-7748 clamping to a 32-byte scalar.
func clamp(k []byte) {
k[0] &= 248
k[31] &= 127
k[31] |= 64
}
// IdentityFromEntropy converts 32 deterministic bytes into an
// *age.X25519Identity by round-tripping through Bech32.
func IdentityFromEntropy(ent []byte) (*age.X25519Identity, error) {
if len(ent) != 32 {
return nil, fmt.Errorf("need 32-byte scalar, got %d", len(ent))
}
// 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)
}
s, err := bech32.Encode(hrp, data)
if err != nil {
return nil, fmt.Errorf("bech32 encode: %w", err)
}
return age.ParseX25519Identity(strings.ToUpper(s))
}
// DeriveEntropy derives 32 bytes of application-scoped entropy from the
// 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, "")
// Create master key from seed
masterKey, err := hdkeychain.NewMaster(seed, &chaincfg.MainNetParams)
if err != nil {
return nil, fmt.Errorf("failed to create 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
}
// 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 {
return nil, err
}
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)
}

928
pkg/agehd/agehd_test.go Normal file
View File

@@ -0,0 +1,928 @@
package agehd
import (
"bytes"
"crypto/rand"
"fmt"
"io"
"strings"
"testing"
"filippo.io/age"
"github.com/tyler-smith/go-bip39"
)
const (
mnemonic = "abandon abandon abandon abandon abandon " +
"abandon abandon abandon abandon abandon abandon about"
// Test xprv from BIP85 test vectors
testXPRV = "xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLjbKb"
// Additional test mnemonics for comprehensive testing
testMnemonic12 = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
testMnemonic15 = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
testMnemonic18 = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
testMnemonic21 = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
testMnemonic24 = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art"
// Test messages used throughout the tests
testMessageHelloWorld = "hello world"
testMessageHelloFromXPRV = "hello from xprv"
testMessageGeneric = "test message"
testMessageBoundary = "boundary test"
testMessageBenchmark = "benchmark test message"
testMessageLargePattern = "A"
// Error messages for validation
errorMsgNeed32Bytes = "need 32-byte scalar, got"
errorMsgInvalidXPRV = "invalid-xprv"
// Test constants for various scenarios
testSkipMessage = "Skipping consistency test - test mnemonic and xprv are from different sources"
// Numeric constants for testing
testNumGoroutines = 10
testNumIterations = 100
// Large data test constants
testDataSizeMegabyte = 1024 * 1024 // 1 MB
)
func TestEncryptDecrypt(t *testing.T) {
id, err := DeriveIdentity(mnemonic, 0)
if err != nil {
t.Fatalf("derive: %v", err)
}
t.Logf("secret: %s", id.String())
t.Logf("recipient: %s", id.Recipient().String())
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, testMessageHelloWorld); 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 != testMessageHelloWorld {
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, testMessageHelloFromXPRV); 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 != testMessageHelloFromXPRV {
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(testSkipMessage)
}
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(errorMsgInvalidXPRV, 0)
if err == nil {
t.Fatalf("expected error for invalid xprv")
}
t.Logf("Got expected error for invalid xprv: %v", err)
}
// TestClampFunction tests the RFC-7748 clamping function
func TestClampFunction(t *testing.T) {
tests := []struct {
name string
input []byte
expected []byte
}{
{
name: "all zeros",
input: make([]byte, 32),
expected: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 64},
},
{
name: "all ones",
input: bytes.Repeat([]byte{255}, 32),
expected: append([]byte{248}, append(bytes.Repeat([]byte{255}, 30), 127)...),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
input := make([]byte, 32)
copy(input, tt.input)
clamp(input)
// Check specific bits that should be clamped
if input[0]&7 != 0 {
t.Errorf("first byte should have bottom 3 bits cleared, got %08b", input[0])
}
if input[31]&128 != 0 {
t.Errorf("last byte should have top bit cleared, got %08b", input[31])
}
if input[31]&64 == 0 {
t.Errorf("last byte should have second-to-top bit set, got %08b", input[31])
}
})
}
}
// TestIdentityFromEntropyEdgeCases tests edge cases for IdentityFromEntropy
func TestIdentityFromEntropyEdgeCases(t *testing.T) {
tests := []struct {
name string
entropy []byte
expectError bool
errorMsg string
}{
{
name: "nil entropy",
entropy: nil,
expectError: true,
errorMsg: errorMsgNeed32Bytes + " 0",
},
{
name: "empty entropy",
entropy: []byte{},
expectError: true,
errorMsg: errorMsgNeed32Bytes + " 0",
},
{
name: "too short entropy",
entropy: make([]byte, 31),
expectError: true,
errorMsg: errorMsgNeed32Bytes + " 31",
},
{
name: "too long entropy",
entropy: make([]byte, 33),
expectError: true,
errorMsg: errorMsgNeed32Bytes + " 33",
},
{
name: "valid 32-byte entropy",
entropy: make([]byte, 32),
expectError: false,
},
{
name: "random valid entropy",
entropy: func() []byte { b := make([]byte, 32); rand.Read(b); return b }(),
expectError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
identity, err := IdentityFromEntropy(tt.entropy)
if tt.expectError {
if err == nil {
t.Errorf("expected error but got none")
} else if !strings.Contains(err.Error(), tt.errorMsg) {
t.Errorf("expected error containing %q, got %q", tt.errorMsg, err.Error())
}
if identity != nil {
t.Errorf("expected nil identity on error, got %v", identity)
}
} else {
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if identity == nil {
t.Errorf("expected valid identity, got nil")
}
}
})
}
}
// TestDeriveEntropyInvalidMnemonic tests error handling for invalid mnemonics
func TestDeriveEntropyInvalidMnemonic(t *testing.T) {
tests := []struct {
name string
mnemonic string
}{
{
name: "empty mnemonic",
mnemonic: "",
},
{
name: "single word",
mnemonic: "abandon",
},
{
name: "invalid word",
mnemonic: "invalid word sequence that does not exist in bip39",
},
{
name: "wrong word count",
mnemonic: "abandon abandon abandon abandon abandon",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Note: BIP39 library is quite permissive and doesn't validate
// mnemonic words strictly, so we mainly test that the function
// doesn't panic and produces some result
entropy, err := DeriveEntropy(tt.mnemonic, 0)
if err != nil {
t.Logf("Got error for invalid mnemonic %q: %v", tt.name, err)
} else {
if len(entropy) != 32 {
t.Errorf("expected 32 bytes even for invalid mnemonic, got %d", len(entropy))
}
t.Logf("Invalid mnemonic %q produced entropy: %x", tt.name, entropy)
}
})
}
}
// TestDeriveEntropyFromXPRVInvalidInputs tests error handling for invalid XPRVs
func TestDeriveEntropyFromXPRVInvalidInputs(t *testing.T) {
tests := []struct {
name string
xprv string
expectError bool
}{
{
name: "empty xprv",
xprv: "",
expectError: true,
},
{
name: "invalid base58",
xprv: "invalid-base58-string-!@#$%",
expectError: true,
},
{
name: "wrong prefix",
xprv: "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8",
expectError: true,
},
{
name: "truncated xprv",
xprv: "xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLj",
expectError: true,
},
{
name: "valid xprv",
xprv: testXPRV,
expectError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
entropy, err := DeriveEntropyFromXPRV(tt.xprv, 0)
if tt.expectError {
if err == nil {
t.Errorf("expected error for invalid xprv %q", tt.name)
} else {
t.Logf("Got expected error for %q: %v", tt.name, err)
}
} else {
if err != nil {
t.Errorf("unexpected error for valid xprv: %v", err)
}
if len(entropy) != 32 {
t.Errorf("expected 32 bytes of entropy, got %d", len(entropy))
}
}
})
}
}
// TestDifferentMnemonicLengths tests derivation with different mnemonic lengths
func TestDifferentMnemonicLengths(t *testing.T) {
mnemonics := map[string]string{
"12 words": testMnemonic12,
"15 words": testMnemonic15,
"18 words": testMnemonic18,
"21 words": testMnemonic21,
"24 words": testMnemonic24,
}
for name, mnemonic := range mnemonics {
t.Run(name, func(t *testing.T) {
identity, err := DeriveIdentity(mnemonic, 0)
if err != nil {
t.Fatalf("failed to derive identity from %s: %v", name, err)
}
// Test that we can encrypt/decrypt
var ct bytes.Buffer
w, err := age.Encrypt(&ct, identity.Recipient())
if err != nil {
t.Fatalf("encrypt init: %v", err)
}
if _, err = io.WriteString(w, testMessageGeneric); 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()), identity)
if err != nil {
t.Fatalf("decrypt init: %v", err)
}
dec, err := io.ReadAll(r)
if err != nil {
t.Fatalf("read: %v", err)
}
if string(dec) != testMessageGeneric {
t.Fatalf("round-trip failed for %s", name)
}
t.Logf("%s identity: %s", name, identity.String())
})
}
}
// TestIndexBoundaries tests derivation with various index values
func TestIndexBoundaries(t *testing.T) {
indices := []uint32{
0, // minimum
1, // basic
100, // moderate
1000, // larger
0x7FFFFFFF, // maximum hardened index
0xFFFFFFFF, // maximum uint32
}
for _, index := range indices {
t.Run(fmt.Sprintf("index_%d", index), func(t *testing.T) {
identity, err := DeriveIdentity(mnemonic, index)
if err != nil {
t.Fatalf("failed to derive identity at index %d: %v", index, err)
}
// Verify the identity is valid by testing encryption/decryption
var ct bytes.Buffer
w, err := age.Encrypt(&ct, identity.Recipient())
if err != nil {
t.Fatalf("encrypt init at index %d: %v", index, err)
}
if _, err = io.WriteString(w, testMessageBoundary); err != nil {
t.Fatalf("write at index %d: %v", index, err)
}
if err = w.Close(); err != nil {
t.Fatalf("encrypt close at index %d: %v", index, err)
}
r, err := age.Decrypt(bytes.NewReader(ct.Bytes()), identity)
if err != nil {
t.Fatalf("decrypt init at index %d: %v", index, err)
}
dec, err := io.ReadAll(r)
if err != nil {
t.Fatalf("read at index %d: %v", index, err)
}
if string(dec) != testMessageBoundary {
t.Fatalf("round-trip failed at index %d", index)
}
t.Logf("Index %d identity: %s", index, identity.String())
})
}
}
// TestEntropyUniqueness tests that different inputs produce different entropy
func TestEntropyUniqueness(t *testing.T) {
// Test different indices with same mnemonic
entropy1, err := DeriveEntropy(mnemonic, 0)
if err != nil {
t.Fatalf("derive entropy 1: %v", err)
}
entropy2, err := DeriveEntropy(mnemonic, 1)
if err != nil {
t.Fatalf("derive entropy 2: %v", err)
}
if bytes.Equal(entropy1, entropy2) {
t.Fatalf("different indices should produce different entropy")
}
// Test different mnemonics with same index
entropy3, err := DeriveEntropy(testMnemonic24, 0)
if err != nil {
t.Fatalf("derive entropy 3: %v", err)
}
if bytes.Equal(entropy1, entropy3) {
t.Fatalf("different mnemonics should produce different entropy")
}
t.Logf("Entropy uniqueness verified across indices and mnemonics")
}
// TestConcurrentDerivation tests that derivation is safe for concurrent use
func TestConcurrentDerivation(t *testing.T) {
results := make(chan string, testNumGoroutines*testNumIterations)
errors := make(chan error, testNumGoroutines*testNumIterations)
for i := 0; i < testNumGoroutines; i++ {
go func(goroutineID int) {
for j := 0; j < testNumIterations; j++ {
identity, err := DeriveIdentity(mnemonic, uint32(j))
if err != nil {
errors <- err
return
}
results <- identity.String()
}
}(i)
}
// Collect results
resultMap := make(map[string]int)
for i := 0; i < testNumGoroutines*testNumIterations; i++ {
select {
case result := <-results:
resultMap[result]++
case err := <-errors:
t.Fatalf("concurrent derivation error: %v", err)
}
}
// Verify that each index produced the same result across all goroutines
expectedResults := testNumGoroutines
for result, count := range resultMap {
if count != expectedResults {
t.Errorf("result %s appeared %d times, expected %d", result, count, expectedResults)
}
}
t.Logf("Concurrent derivation test passed with %d unique results", len(resultMap))
}
// Benchmark tests
func BenchmarkDeriveIdentity(b *testing.B) {
for i := 0; i < b.N; i++ {
_, err := DeriveIdentity(mnemonic, uint32(i%1000))
if err != nil {
b.Fatalf("derive identity: %v", err)
}
}
}
func BenchmarkDeriveIdentityFromXPRV(b *testing.B) {
for i := 0; i < b.N; i++ {
_, err := DeriveIdentityFromXPRV(testXPRV, uint32(i%1000))
if err != nil {
b.Fatalf("derive identity from xprv: %v", err)
}
}
}
func BenchmarkDeriveEntropy(b *testing.B) {
for i := 0; i < b.N; i++ {
_, err := DeriveEntropy(mnemonic, uint32(i%1000))
if err != nil {
b.Fatalf("derive entropy: %v", err)
}
}
}
func BenchmarkIdentityFromEntropy(b *testing.B) {
entropy := make([]byte, 32)
rand.Read(entropy)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := IdentityFromEntropy(entropy)
if err != nil {
b.Fatalf("identity from entropy: %v", err)
}
}
}
func BenchmarkEncryptDecrypt(b *testing.B) {
identity, err := DeriveIdentity(mnemonic, 0)
if err != nil {
b.Fatalf("derive identity: %v", err)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
var ct bytes.Buffer
w, err := age.Encrypt(&ct, identity.Recipient())
if err != nil {
b.Fatalf("encrypt init: %v", err)
}
if _, err = io.WriteString(w, testMessageBenchmark); err != nil {
b.Fatalf("write: %v", err)
}
if err = w.Close(); err != nil {
b.Fatalf("encrypt close: %v", err)
}
r, err := age.Decrypt(bytes.NewReader(ct.Bytes()), identity)
if err != nil {
b.Fatalf("decrypt init: %v", err)
}
_, err = io.ReadAll(r)
if err != nil {
b.Fatalf("read: %v", err)
}
}
}
// TestConstants verifies the hardcoded constants
func TestConstants(t *testing.T) {
if purpose != 83696968 {
t.Errorf("purpose constant mismatch: expected 83696968, got %d", purpose)
}
if vendorID != 592366788 {
t.Errorf("vendorID constant mismatch: expected 592366788, got %d", vendorID)
}
if appID != 733482323 {
t.Errorf("appID constant mismatch: expected 733482323, got %d", appID)
}
if hrp != "age-secret-key-" {
t.Errorf("hrp constant mismatch: expected 'age-secret-key-', got %q", hrp)
}
}
// TestIdentityStringFormat tests that generated identities have the correct format
func TestIdentityStringFormat(t *testing.T) {
identity, err := DeriveIdentity(mnemonic, 0)
if err != nil {
t.Fatalf("derive identity: %v", err)
}
secretKey := identity.String()
recipient := identity.Recipient().String()
// Check secret key format
if !strings.HasPrefix(secretKey, "AGE-SECRET-KEY-") {
t.Errorf("secret key should start with 'AGE-SECRET-KEY-', got: %s", secretKey)
}
// Check recipient format
if !strings.HasPrefix(recipient, "age1") {
t.Errorf("recipient should start with 'age1', got: %s", recipient)
}
// Check that they're different
if secretKey == recipient {
t.Errorf("secret key and recipient should be different")
}
t.Logf("Secret key format: %s", secretKey)
t.Logf("Recipient format: %s", recipient)
}
// TestLargeMessageEncryption tests encryption/decryption of larger messages
func TestLargeMessageEncryption(t *testing.T) {
identity, err := DeriveIdentity(mnemonic, 0)
if err != nil {
t.Fatalf("derive identity: %v", err)
}
// Test with different message sizes
sizes := []int{1, 100, 1024, 10240, 100000}
for _, size := range sizes {
t.Run(fmt.Sprintf("size_%d", size), func(t *testing.T) {
message := strings.Repeat(testMessageLargePattern, size)
var ct bytes.Buffer
w, err := age.Encrypt(&ct, identity.Recipient())
if err != nil {
t.Fatalf("encrypt init: %v", err)
}
if _, err = io.WriteString(w, message); 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()), identity)
if err != nil {
t.Fatalf("decrypt init: %v", err)
}
dec, err := io.ReadAll(r)
if err != nil {
t.Fatalf("read: %v", err)
}
if string(dec) != message {
t.Fatalf("message size %d: round-trip failed", size)
}
t.Logf("Successfully encrypted/decrypted %d byte message", size)
})
}
}
// TestRandomMnemonicDeterministicGeneration tests that:
// 1. A random mnemonic generates the same keys deterministically
// 2. Large data (1MB) can be encrypted and decrypted successfully
func TestRandomMnemonicDeterministicGeneration(t *testing.T) {
// Generate a random mnemonic using the BIP39 library
entropy := make([]byte, 32) // 256 bits for 24-word mnemonic
_, err := rand.Read(entropy)
if err != nil {
t.Fatalf("failed to generate random entropy: %v", err)
}
randomMnemonic, err := bip39.NewMnemonic(entropy)
if err != nil {
t.Fatalf("failed to generate random mnemonic: %v", err)
}
t.Logf("Generated random mnemonic: %s", randomMnemonic)
// Test index for key derivation
testIndex := uint32(42)
// Generate the first identity
identity1, err := DeriveIdentity(randomMnemonic, testIndex)
if err != nil {
t.Fatalf("failed to derive first identity: %v", err)
}
// Generate the second identity with the same mnemonic and index
identity2, err := DeriveIdentity(randomMnemonic, testIndex)
if err != nil {
t.Fatalf("failed to derive second identity: %v", err)
}
// Verify that both private keys are identical
privateKey1 := identity1.String()
privateKey2 := identity2.String()
if privateKey1 != privateKey2 {
t.Fatalf("private keys should be identical:\nFirst: %s\nSecond: %s", privateKey1, privateKey2)
}
// Verify that both public keys (recipients) are identical
publicKey1 := identity1.Recipient().String()
publicKey2 := identity2.Recipient().String()
if publicKey1 != publicKey2 {
t.Fatalf("public keys should be identical:\nFirst: %s\nSecond: %s", publicKey1, publicKey2)
}
t.Logf("✓ Deterministic generation verified")
t.Logf("Private key: %s", privateKey1)
t.Logf("Public key: %s", publicKey1)
// Generate 1 MB of random data for encryption test
testData := make([]byte, testDataSizeMegabyte)
_, err = rand.Read(testData)
if err != nil {
t.Fatalf("failed to generate random test data: %v", err)
}
t.Logf("Generated %d bytes of random test data", len(testData))
// Encrypt the data using the public key (recipient)
var ciphertext bytes.Buffer
encryptor, err := age.Encrypt(&ciphertext, identity1.Recipient())
if err != nil {
t.Fatalf("failed to create encryptor: %v", err)
}
_, err = encryptor.Write(testData)
if err != nil {
t.Fatalf("failed to write data to encryptor: %v", err)
}
err = encryptor.Close()
if err != nil {
t.Fatalf("failed to close encryptor: %v", err)
}
t.Logf("✓ Encrypted %d bytes into %d bytes of ciphertext", len(testData), ciphertext.Len())
// Decrypt the data using the private key
decryptor, err := age.Decrypt(bytes.NewReader(ciphertext.Bytes()), identity1)
if err != nil {
t.Fatalf("failed to create decryptor: %v", err)
}
decryptedData, err := io.ReadAll(decryptor)
if err != nil {
t.Fatalf("failed to read decrypted data: %v", err)
}
t.Logf("✓ Decrypted %d bytes", len(decryptedData))
// Verify that the decrypted data matches the original
if len(decryptedData) != len(testData) {
t.Fatalf("decrypted data length mismatch: expected %d, got %d", len(testData), len(decryptedData))
}
if !bytes.Equal(testData, decryptedData) {
t.Fatalf("decrypted data does not match original data")
}
t.Logf("✓ Large data encryption/decryption test passed successfully")
// Additional verification: test with the second identity (should work identically)
var ciphertext2 bytes.Buffer
encryptor2, err := age.Encrypt(&ciphertext2, identity2.Recipient())
if err != nil {
t.Fatalf("failed to create second encryptor: %v", err)
}
_, err = encryptor2.Write(testData)
if err != nil {
t.Fatalf("failed to write data to second encryptor: %v", err)
}
err = encryptor2.Close()
if err != nil {
t.Fatalf("failed to close second encryptor: %v", err)
}
// Decrypt with the second identity
decryptor2, err := age.Decrypt(bytes.NewReader(ciphertext2.Bytes()), identity2)
if err != nil {
t.Fatalf("failed to create second decryptor: %v", err)
}
decryptedData2, err := io.ReadAll(decryptor2)
if err != nil {
t.Fatalf("failed to read second decrypted data: %v", err)
}
if !bytes.Equal(testData, decryptedData2) {
t.Fatalf("second decrypted data does not match original data")
}
t.Logf("✓ Cross-verification with second identity successful")
}

152
pkg/bip85/README.md Normal file
View File

@@ -0,0 +1,152 @@
# BIP85 - Deterministic Entropy From BIP32 Keychains
This package implements [BIP85](https://github.com/bitcoin/bips/blob/master/bip-0085.mediawiki), which allows for deterministic derivation of entropy from a BIP32 master key. This enables a single seed to generate multiple wallet keys, mnemonics, and random values in a fully deterministic way.
## Overview
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 hex values and passwords
## Usage Examples
### Initialization
```go
import (
"fmt"
"git.eeqj.de/sneak/secret/pkg/bip85"
"github.com/btcsuite/btcd/btcutil/hdkeychain"
)
// Parse an existing master key
masterKeyStr := "xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLjbKb"
masterKey, err := bip85.ParseMasterKey(masterKeyStr)
if err != nil {
panic(err)
}
```
### Derive BIP39 Mnemonic
```go
// Parameters:
// - language (0 = English, 1 = Japanese, 2 = Korean, 3 = Spanish, etc.)
// - number of words (12, 15, 18, 21, or 24)
// - index (allows multiple seeds of the same type)
entropy, err := bip85.DeriveBIP39Entropy(masterKey, 0, 12, 0)
if err != nil {
panic(err)
}
// Use the entropy with github.com/tyler-smith/go-bip39
// to generate a mnemonic
mnemonic, err := bip39.NewMnemonic(entropy)
if err != nil {
panic(err)
}
fmt.Println("12-word BIP39 mnemonic:", mnemonic)
```
### Derive HD-WIF Key
```go
// Create a WIF format key for Bitcoin Core's hdseed
wif, err := bip85.DeriveWIFKey(masterKey, 0)
if err != nil {
panic(err)
}
fmt.Println("WIF Key:", wif)
```
### Derive XPRV
```go
// Create an extended private key (XPRV)
xprv, err := bip85.DeriveXPRV(masterKey, 0)
if err != nil {
panic(err)
}
fmt.Println("XPRV:", xprv.String())
```
### Generate Hex Data
```go
// Generate arbitrary hex data (16-64 bytes)
hex, err := bip85.DeriveHex(masterKey, 32, 0)
if err != nil {
panic(err)
}
fmt.Println("32 bytes of hex:", hex)
```
### DRNG (Deterministic Random Number Generator)
```go
// First derive entropy
path := "m/83696968'/0'/0'"
entropy, err := bip85.DeriveBIP85Entropy(masterKey, path)
if err != nil {
panic(err)
}
// Create a deterministic random number generator
drng := bip85.NewBIP85DRNG(entropy)
// Read arbitrary amount of random bytes
buffer := make([]byte, 32)
_, err = drng.Read(buffer)
if err != nil {
panic(err)
}
fmt.Printf("Random bytes: %x\n", buffer)
```
## BIP85 Paths
The derivation paths follow the format:
```
m/83696968'/{app}'/{parameters}
```
Where:
- `83696968'` is the BIP85 root path (BIP in ASCII)
- `{app}'` is the application number:
- `39'` for BIP39 mnemonics
- `2'` for HD-WIF keys
- `32'` for XPRV
- `128169'` for HEX data
- `707764'` for Base64 passwords
- `707785'` for Base85 passwords
- `828365'` for RSA keys
- `{parameters}` are application-specific parameters
## Test Vectors
This implementation passes all the test vectors from the BIP85 specification:
- Basic test cases
- BIP39 12, 18, and 24 word mnemonics
- HD-WIF keys
- XPRV
- SHAKE256 DRNG output
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:
```
go test -v git.eeqj.de/sneak/secret/pkg/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

@@ -0,0 +1,479 @@
<pre>
BIP: 85
Layer: Applications
Title: Deterministic Entropy From BIP32 Keychains
Author: Ethan Kosakovsky <ethankosakovsky@protonmail.com>
Aneesh Karve <dowsing.seaport0d@icloud.com>
Comments-Summary: No comments yet.
Comments-URI: https://github.com/bitcoin/bips/wiki/Comments:BIP-0085
Status: Final
Type: Informational
Created: 2020-03-20
License: BSD-2-Clause
OPL
</pre>
==Abstract==
''"One Seed to rule them all,''<br>
''One Key to find them,''<br>
''One Path to bring them all,''<br>
''And in cryptography bind them."''
It is not possible to maintain one single (mnemonic) seed backup for all keychains used across various wallets because there are a variety of incompatible standards. Sharing of seeds across multiple wallets is not desirable for security reasons. Physical storage of multiple seeds is difficult depending on the security and redundancy required.
As HD keychains are essentially derived from initial entropy, this proposal provides a way to derive entropy from the keychain which can be fed into whatever method a wallet uses to derive the initial mnemonic seed or root key.
==Copyright==
This BIP is dual-licensed under the Open Publication License and BSD 2-clause license.
==Definitions==
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.
The terminology related to keychains used in the wild varies widely, for example `seed` has various different meanings. In this document we define the terms
# '''BIP32 root key''' is the root extended private key that is represented as the top root of the keychain in BIP32.
# '''BIP39 mnemonic''' is the mnemonic phrase that is calculated from the entropy used before hashing of the mnemonic in BIP39.
# '''BIP39 seed''' is the result of hashing the BIP39 mnemonic seed.
When in doubt, assume big endian byte serialization, such that the leftmost
byte is the most significant.
==Motivation==
Most wallets implement BIP32 which defines how a BIP32 root key can be used to derive keychains. As a consequence, a backup of just the BIP32 root key is sufficient to include all keys derived from it. BIP32 does not have a human-friendly serialization of the BIP32 root key (or BIP32 extended keys in general), which makes paper backups or manually restoring the key more error-prone. BIP39 was designed to solve this problem, but rather than serialize the BIP32 root key, it takes some entropy, encoded to a "seed mnemonic", which is then hashed to derive the BIP39 seed, which can be turned into the BIP32 root key. Saving the BIP39 mnemonic is enough to reconstruct the entire BIP32 keychain, but a BIP32 root key cannot be reversed back to the BIP39 mnemonic.
Most wallets implement BIP39, so on initialization or restoration, the user must interact with a BIP39 mnemonic. Most wallets do not support BIP32 extended private keys, so each wallet must either share the same BIP39 mnemonic, or have a separate BIP39 mnemonic entirely. Neither scenario is particularly satisfactory for security reasons. For example, some wallets may be inherently less secure, like hot wallets on smartphones, JoinMarket servers, or Lightning Network nodes. Having multiple seeds is far from desirable, especially for those who rely on split key or redundancy backups in different geological locations. Adding keys is necessarily difficult and may result in users being more lazy with subsequent keys, resulting in compromised security or loss of keys.
There is an added complication with wallets that implement other standards, or no standards at all. The Bitcoin Core wallet uses a WIF as the ''hdseed'', and yet other wallets, like Electrum, use different mnemonic schemes to derive the BIP32 root key. Other cryptocurrencies, like Monero, use an entirely different mnemonic scheme.
Ultimately, all of the mnemonic/seed schemes start with some "initial entropy" to derive a mnemonic/seed, and then process the mnemonic into a BIP32 key, or private key. We can use BIP32 itself to derive the "initial entropy" to then recreate the same mnemonic or seed according to the specific application standard of the target wallet. We can use a BIP44-like categorization to ensure uniform derivation according to the target application type.
==Specification==
We assume a single BIP32 master root key. This specification is not concerned with how this was derived (e.g. directly or via a mnemonic scheme such as BIP39).
For each application that requires its own wallet, a unique private key is derived from the BIP32 master root key using a fully hardened derivation path. The resulting private key (k) is then processed with HMAC-SHA512, where the key is "bip-entropy-from-k", and the message payload is the private key k: <code>HMAC-SHA512(key="bip-entropy-from-k", msg=k)</code>
<ref name="hmac-sha512">
The reason for running the derived key through HMAC-SHA512 and truncating the result as necessary is to prevent leakage of the parent tree should the derived key (''k'') be compromised. While the specification requires the use of hardened key derivation which would prevent this, we cannot enforce hardened derivation, so this method ensures the derived entropy is hardened. Also, from a semantic point of view, since the purpose is to derive entropy and not a private key, we are required to transform the child key. This is done out of an abundance of caution, in order to ward off unwanted side effects should ''k'' be used for a dual purpose, including as a nonce ''hash(k)'', where undesirable and unforeseen interactions could occur.
</ref>.
The result produces 512 bits of entropy. Each application SHOULD use up to the required number of bits necessary for their operation, and truncate the rest.
The HMAC-SHA512 function is specified in [https://tools.ietf.org/html/rfc4231 RFC 4231].
===Test vectors===
====Test case 1====
INPUT:
* MASTER BIP32 ROOT KEY: xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLjbKb
* PATH: m/83696968'/0'/0'
OUTPUT:
* DERIVED KEY=cca20ccb0e9a90feb0912870c3323b24874b0ca3d8018c4b96d0b97c0e82ded0
* DERIVED ENTROPY=efecfbccffea313214232d29e71563d941229afb4338c21f9517c41aaa0d16f00b83d2a09ef747e7a64e8e2bd5a14869e693da66ce94ac2da570ab7ee48618f7
====Test case 2====
INPUT:
* MASTER BIP32 ROOT KEY: xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLjbKb
*PATH: m/83696968'/0'/1'
OUTPUT
* DERIVED KEY=503776919131758bb7de7beb6c0ae24894f4ec042c26032890c29359216e21ba
* DERIVED ENTROPY=70c6e3e8ebee8dc4c0dbba66076819bb8c09672527c4277ca8729532ad711872218f826919f6b67218adde99018a6df9095ab2b58d803b5b93ec9802085a690e
==BIP85-DRNG==
BIP85-DRNG-SHAKE256 is a deterministic random number generator for cryptographic functions that require deterministic outputs, but where the input to that function requires more than the 64 bytes provided by BIP85's HMAC output. BIP85-DRNG-SHAKE256 uses BIP85 to seed a SHAKE256 stream (from the SHA-3 standard). The input must be exactly 64 bytes long (from the BIP85 HMAC output).
RSA key generation is an example of a function that requires orders of magnitude more than 64 bytes of random input. Further, it is not possible to precalculate the amount of random input required until the function has completed.
drng_reader = BIP85DRNG.new(bip85_entropy)
rsa_key = RSA.generate_key(4096, drng_reader.read)
===Test Vectors===
INPUT:
xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLjbKb
* MASTER BIP32 ROOT KEY: m/83696968'/0'/0'
OUTPUT
* DERIVED KEY=cca20ccb0e9a90feb0912870c3323b24874b0ca3d8018c4b96d0b97c0e82ded0
* DERIVED ENTROPY=efecfbccffea313214232d29e71563d941229afb4338c21f9517c41aaa0d16f00b83d2a09ef747e7a64e8e2bd5a14869e693da66ce94ac2da570ab7ee48618f7
* DRNG(80 bytes)=b78b1ee6b345eae6836c2d53d33c64cdaf9a696487be81b03e822dc84b3f1cd883d7559e53d175f243e4c349e822a957bbff9224bc5dde9492ef54e8a439f6bc8c7355b87a925a37ee405a7502991111
==Applications==
The Application number defines how entropy will be used post processing. Some basic examples follow:
Derivation paths follow the format <code>m/83696968'/{app_no}'/{index}'</code>, where ''{app_no}'' is the path for the application, and ''{index}'' is the index.
Application numbers should be semantic in some way, such as a BIP number or ASCII character code sequence.
===BIP39===
Application number: 39'
Truncate trailing (least significant) bytes of the entropy to the number of bits required to map to the relevant word length: 128 bits for 12 words, 256 bits for 24 words.
The derivation path format is: <code>m/83696968'/39'/{language}'/{words}'/{index}'</code>
Example: a BIP39 mnemonic with 12 English words (first index) would have the path <code>m/83696968'/39'/0'/12'/0'</code>, the next key would be <code>m/83696968'/39'/0'/12'/1'</code> etc.
Language Table
{|
!Wordlist
!Code
|-
| English
| 0'
|-
| Japanese
| 1'
|-
| Korean
| 2'
|-
| Spanish
| 3'
|-
| Chinese (Simplified)
| 4'
|-
| Chinese (Traditional)
| 5'
|-
| French
| 6'
|-
| Italian
| 7'
|-
| Czech
| 8'
|-
| Portuguese
| 9'
|-
|}
Words Table
{|
!Words
!Entropy
!Code
|-
| 12 words
| 128 bits
| 12'
|-
| 15 words
| 160 bits
| 15'
|-
| 18 words
| 192 bits
| 18'
|-
| 21 words
| 224 bits
| 21'
|-
| 24 words
| 256 bits
| 24'
|}
====12 English words====
BIP39 English 12 word mnemonic seed
128 bits of entropy as input to BIP39 to derive 12 word mnemonic
INPUT:
* MASTER BIP32 ROOT KEY: xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLjbKb
* PATH: m/83696968'/39'/0'/12'/0'
OUTPUT:
* DERIVED ENTROPY=6250b68daf746d12a24d58b4787a714b
* DERIVED BIP39 MNEMONIC=girl mad pet galaxy egg matter matrix prison refuse sense ordinary nose
====18 English words====
BIP39 English 18 word mnemonic seed
196 bits of entropy as input to BIP39 to derive 18 word mnemonic
INPUT:
* MASTER BIP32 ROOT KEY: xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLjbKb
* PATH: m/83696968'/39'/0'/18'/0'
OUTPUT:
* DERIVED ENTROPY=938033ed8b12698449d4bbca3c853c66b293ea1b1ce9d9dc
* DERIVED BIP39 MNEMONIC=near account window bike charge season chef number sketch tomorrow excuse sniff circle vital hockey outdoor supply token
====24 English words====
Derives 24 word BIP39 mnemonic seed
256 bits of entropy as input to BIP39 to derive 24 word mnemonic
INPUT:
* MASTER BIP32 ROOT KEY: xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLjbKb
* PATH: m/83696968'/39'/0'/24'/0'
OUTPUT:
* DERIVED ENTROPY=ae131e2312cdc61331542efe0d1077bac5ea803adf24b313a4f0e48e9c51f37f
* DERIVED BIP39 MNEMONIC=puppy ocean match cereal symbol another shed magic wrap hammer bulb intact gadget divorce twin tonight reason outdoor destroy simple truth cigar social volcano
===HD-Seed WIF===
Application number: 2'
Uses the most significant 256 bits<ref name="curve-order">
There is a very small chance that you'll make an invalid
key that is zero or larger than the order of the curve. If this occurs, software
should hard fail (forcing users to iterate to the next index). From BIP32:
<blockquote>
In case parse<sub>256</sub>(I<sub>L</sub>) ≥ n or k<sub>i</sub> = 0, the resulting key is invalid, and one should proceed with the next value for i. (Note: this has probability lower than 1 in 2<sup>127</sup>.)
</blockquote>
</ref>
of entropy as the secret exponent to derive a private key and encode as a compressed
WIF that will be used as the hdseed for Bitcoin Core wallets.
Path format is <code>m/83696968'/2'/{index}'</code>
INPUT:
* MASTER BIP32 ROOT KEY: xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLjbKb
* PATH: m/83696968'/2'/0'
OUTPUT
* DERIVED ENTROPY=7040bb53104f27367f317558e78a994ada7296c6fde36a364e5baf206e502bb1
* DERIVED WIF=Kzyv4uF39d4Jrw2W7UryTHwZr1zQVNk4dAFyqE6BuMrMh1Za7uhp
===XPRV===
Application number: 32'
Taking 64 bytes of the HMAC digest, the first 32 bytes are the chain code, and the second 32 bytes<ref name="curve-order" /> are the private key for the BIP32 XPRV value. Child number, depth, and parent fingerprint are forced to zero.
''Warning'': The above order reverses the order of BIP32, which takes the first 32 bytes as the private key, and the second 32 bytes as the chain code.
Applications may support Testnet by emitting TPRV keys if and only if the input root key is a Testnet key.
Path format is <code>m/83696968'/32'/{index}'</code>
INPUT:
* MASTER BIP32 ROOT KEY: xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLjbKb
* PATH: m/83696968'/32'/0'
OUTPUT
* DERIVED ENTROPY=ead0b33988a616cf6a497f1c169d9e92562604e38305ccd3fc96f2252c177682
* DERIVED XPRV=xprv9s21ZrQH143K2srSbCSg4m4kLvPMzcWydgmKEnMmoZUurYuBuYG46c6P71UGXMzmriLzCCBvKQWBUv3vPB3m1SATMhp3uEjXHJ42jFg7myX
===HEX===
Application number: 128169'
The derivation path format is: <code>m/83696968'/128169'/{num_bytes}'/{index}'</code>
`16 <= num_bytes <= 64`
Truncate trailing (least significant) bytes of the entropy after `num_bytes`.
INPUT:
* MASTER BIP32 ROOT KEY: xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLjbKb
* PATH: m/83696968'/128169'/64'/0'
OUTPUT
* DERIVED ENTROPY=492db4698cf3b73a5a24998aa3e9d7fa96275d85724a91e71aa2d645442f878555d078fd1f1f67e368976f04137b1f7a0d19232136ca50c44614af72b5582a5c
===PWD BASE64===
Application number: 707764'
The derivation path format is: <code>m/83696968'/707764'/{pwd_len}'/{index}'</code>
`20 <= pwd_len <= 86`
[https://datatracker.ietf.org/doc/html/rfc4648 Base64] encode all 64 bytes of entropy.
Remove any spaces or new lines inserted by Base64 encoding process. Slice base64 result string
on index 0 to `pwd_len`. This slice is the password. As `pwd_len` is limited to 86, passwords will not contain padding.
Entropy calculation:<br>
R = 64 (base64 - do not count padding)<br>
L = pwd_len<br>
Entropy = log2(R ** L)<br>
{| class="wikitable" style="margin:auto"
! pwd_length !! (cca) entropy
|-
| 20 || 120.0
|-
| 24 || 144.0
|-
| 32 || 192.0
|-
| 64 || 384.0
|-
| 86 || 516.0
|}
INPUT:
* MASTER BIP32 ROOT KEY: xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLjbKb
* PATH: m/83696968'/707764'/21'/0'
OUTPUT
* DERIVED ENTROPY=74a2e87a9ba0cdd549bdd2f9ea880d554c6c355b08ed25088cfa88f3f1c4f74632b652fd4a8f5fda43074c6f6964a3753b08bb5210c8f5e75c07a4c2a20bf6e9
* DERIVED PWD=dKLoepugzdVJvdL56ogNV
===PWD BASE85===
Application number: 707785'
The derivation path format is: <code>m/83696968'/707785'/{pwd_len}'/{index}'</code>
`10 <= pwd_len <= 80`
Base85 encode all 64 bytes of entropy.
Remove any spaces or new lines inserted by Base64 encoding process. Slice base85 result string
on index 0 to `pwd_len`. This slice is the password. `pwd_len` is limited to 80 characters.
Entropy calculation:<br>
R = 85<br>
L = pwd_len<br>
Entropy = log2(R ** L)<br>
{| class="wikitable" style="margin:auto"
! pwd_length !! (cca) entropy
|-
| 10 || 64.0
|-
| 15 || 96.0
|-
| 20 || 128.0
|-
| 30 || 192.0
|-
| 80 || 512.0
|}
INPUT:
* MASTER BIP32 ROOT KEY: xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLjbKb
* PATH: m/83696968'/707785'/12'/0'
OUTPUT
* DERIVED ENTROPY=f7cfe56f63dca2490f65fcbf9ee63dcd85d18f751b6b5e1c1b8733af6459c904a75e82b4a22efff9b9e69de2144b293aa8714319a054b6cb55826a8e51425209
* DERIVED PWD=_s`{TW89)i4`
===RSA===
Application number: 828365'
The derivation path format is: <code>m/83696968'/828365'/{key_bits}'/{key_index}'</code>
The RSA key generator should use BIP85-DRNG as the input RNG function.
===RSA GPG===
Keys allocated for RSA-GPG purposes use the following scheme:
- Main key <code>m/83696968'/828365'/{key_bits}'/{key_index}'</code>
- Sub keys: <code>m/83696968'/828365'/{key_bits}'/{key_index}'/{sub_key}'</code>
- key_index is the parent key for CERTIFY capability
- sub_key <code>0'</code> is used as the ENCRYPTION key
- sub_key <code>1'</code> is used as the AUTHENTICATION key
- sub_key <code>2'</code> is usually used as SIGNATURE key
Note on timestamps:
The resulting RSA key can be used to create a GPG key where the creation date MUST be fixed to unix Epoch timestamp 1231006505 (the Bitcoin genesis block time <code>'2009-01-03 18:05:05'</code> UTC) because the key fingerprint is affected by the creation date (Epoch timestamp 0 was not chosen because of legacy behavior in GNUPG implementations for older keys). Additionally, when importing sub-keys under a key in GNUPG, the system time must be frozen to the same timestamp before importing (e.g. by use of <code>faketime</code>).
Note on GPG key capabilities on smartcard/hardware devices:
GPG capable smart-cards SHOULD be loaded as follows: The encryption slot SHOULD be loaded with the ENCRYPTION capable key; the authentication slot SHOULD be loaded with the AUTHENTICATION capable key. The signature capable slot SHOULD be loaded with the SIGNATURE capable key.
However, depending on available slots on the smart-card, and preferred policy, the CERTIFY capable key MAY be flagged with CERTIFY and SIGNATURE capabilities and loaded into the SIGNATURE capable slot (for example where the smart-card has only three slots and the CERTIFY capability is required on the same card). In this case, the SIGNATURE capable sub-key would be disregarded because the CERTIFY capable key serves a dual purpose.
===DICE===
Application number: 89101'
The derivation path format is: <code>m/83696968'/89101'/{sides}'/{rolls}'/{index}'</code>
2 <= sides <= 2^32 - 1
1 <= rolls <= 2^32 - 1
Use this application to generate PIN numbers, numeric secrets, and secrets over custom alphabets.
For example, applications could generate alphanumeric passwords from a 62-sided die (26 + 26 + 10).
Roll values are zero-indexed, such that an N-sided die produces values in the range
<code>[0, N-1]</code>, inclusive. Applications should separate printed rolls by a comma or similar.
Create a BIP85 DRNG whose seed is the derived entropy.
Calculate the following integers:
bits_per_roll = ceil(log_2(sides))
bytes_per_roll = ceil(bits_per_roll / 8)
Read <code>bytes_per_roll</code> bytes from the DRNG.
Trim any bits in excess of <code>bits_per_roll</code> (retain the most
significant bits). The resulting integer represents a single roll or trial.
If the trial is greater than or equal to the number of sides, skip it and
move on to the next one. Repeat as needed until all rolls are complete.
INPUT:
* MASTER BIP32 ROOT KEY: xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLjbKb
* PATH: m/83696968'/89101'/6'/10'/0'
OUTPUT
* DERIVED ENTROPY=5e41f8f5d5d9ac09a20b8a5797a3172b28c806aead00d27e36609e2dd116a59176a738804236586f668da8a51b90c708a4226d7f92259c69f64c51124b6f6cd2
* DERIVED ROLLS=1,0,0,2,0,1,5,5,2,4
==Backwards Compatibility==
This specification is not backwards compatible with any other existing specification.
This specification relies on BIP32 but is agnostic to how the BIP32 root key is derived. As such, this standard is able to derive wallets with initialization schemes like BIP39 or Electrum wallet style mnemonics.
==References==
BIP32, BIP39
==Reference Implementations==
* 1.3.0 Python 3.x library implementation: [https://github.com/akarve/bipsea]
* 1.1.0 Python 2.x library implementation: [https://github.com/ethankosakovsky/bip85]
* 1.0.0 JavaScript library implementation: [https://github.com/hoganri/bip85-js]
==Changelog==
===1.3.0 (2024-10-22)===
====Added====
* Dice application 89101'
* Czech language code to application 39'
* TPRV guidance for application 32'
* Warning on application 32' key and chain code ordering
===1.2.0 (2022-12-04)===
====Added====
* Base64 application 707764'
* Base85 application 707785'
===1.1.0 (2020-11-19)===
====Added====
* BIP85-DRNG-SHAKE256
* RSA application 828365'
===1.0.0 (2020-06-11)===
* Initial version
==Footnotes==
<references />
==Acknowledgements==
Many thanks to Peter Gray and Christopher Allen for their input, and to Peter for suggesting extra application use cases.

376
pkg/bip85/bip85.go Normal file
View File

@@ -0,0 +1,376 @@
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)
}

1094
pkg/bip85/bip85_test.go Normal file

File diff suppressed because it is too large Load Diff