Skip to content

Commit

Permalink
WIP: go/common/crypto/sakg: Add BIP32Path type
Browse files Browse the repository at this point in the history
TODO: Add docstrings.
  • Loading branch information
tjanez committed May 6, 2021
1 parent de103d6 commit 3e6c55b
Show file tree
Hide file tree
Showing 2 changed files with 143 additions and 0 deletions.
87 changes: 87 additions & 0 deletions go/common/crypto/sakg/bip32.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package sakg

import (
"fmt"
"strconv"
"strings"
)

// HardenedKeysIndexStart is the index of the first hardened BIP-0032 key.
const HardenedKeysIndexStart = uint32(0x80000000)

const BIP32PathMnemonicComponent = "m"

type BIP32Path []uint32

func (path BIP32Path) String() string {
pathStr := "m"
for _, component := range path {
if component >= HardenedKeysIndexStart {
component -= HardenedKeysIndexStart
pathStr += fmt.Sprintf("/%d'", component)
} else {
pathStr += fmt.Sprintf("/%d", component)
}
}
return pathStr
}

func (path BIP32Path) MarshalText() ([]byte, error) {
return []byte(path.String()), nil
}

func (path *BIP32Path) UnmarshalText(text []byte) error {
components := strings.Split(string(text), "/")
// NOTE: The first component should be the mnemonic component which doesn't
// have a corresponding element in BIP32Path's slice.
n := len(components) - 1
rawPath := make([]uint32, n)

for i, component := range components {
if i == 0 {
if component != BIP32PathMnemonicComponent {
return fmt.Errorf(
"invalid BIP-0032 path's mnemonic component: %s (expected: %s)",
component,
BIP32PathMnemonicComponent,
)
}
} else {
hardened := false
if strings.HasSuffix(component, "'") {
hardened = true
component = strings.TrimSuffix(component, "'")
}
comp64, err := strconv.ParseUint(component, 10, 32)
if err != nil {
return fmt.Errorf("invalid BIP-0032 path's %d. component: %w",
i+1,
err,
)
}
comp32 := uint32(comp64)
if comp32 >= HardenedKeysIndexStart {
return fmt.Errorf(
"invalid BIP-0032 path's %d. component: maximum value of %d exceeded (got: %d)",
i+1,
HardenedKeysIndexStart-1,
comp32,
)
}
if hardened {
comp32 |= HardenedKeysIndexStart
}
rawPath[i-1] = comp32
}
}
*path = BIP32Path(rawPath)
return nil
}

func NewBIP32Path(pathStr string) BIP32Path {
var path BIP32Path
if err := path.UnmarshalText([]byte(pathStr)); err != nil {
panic(err)
}
return path
}
56 changes: 56 additions & 0 deletions go/common/crypto/sakg/bip32_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package sakg

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestBIP32Path(t *testing.T) {
require := require.New(t)

testVectors := []struct {
strPath string
strPathValid bool
path BIP32Path
errMsg string
}{
{"m", true, []uint32{}, ""},
{"m/1/2/3", true, []uint32{1, 2, 3}, ""},
{"m/44'", true, []uint32{0x8000002C}, ""},
{"m/44'/0'", true, []uint32{0x8000002C, 0x80000000}, ""},
{"m/44'/0'/0'", true, []uint32{0x8000002C, 0x80000000, 0x80000000}, ""},
{"m/44'/0'/0'/0", true, []uint32{0x8000002C, 0x80000000, 0x80000000, 0}, ""},
{"m/44'/0'/0'/0/0", true, []uint32{0x8000002C, 0x80000000, 0x80000000, 0, 0}, ""},
{"m/44'/2147483647", true, []uint32{0x8000002C, 0x7FFFFFFF}, ""},
{"m/44'/2147483647'", true, []uint32{0x8000002C, 0xFFFFFFFF}, ""},

{"", false, []uint32{}, "invalid BIP-0032 path's mnemonic component: (expected: m)"},
{"44'/0'", false, []uint32{}, "invalid BIP-0032 path's mnemonic component: 44' (expected: m)"},
{"foo/44'", false, []uint32{}, "invalid BIP-0032 path's mnemonic component: foo (expected: m)"},
{"m/bla'", false, []uint32{}, "invalid BIP-0032 path's 2. component: strconv.ParseUint: parsing \"bla\": invalid syntax"},
{"m/44'/2147483648", false, []uint32{}, "invalid BIP-0032 path's 3. component: maximum value of 2147483647 exceeded (got: 2147483648)"},
{"m/44'/2147483648'", false, []uint32{}, "invalid BIP-0032 path's 3. component: maximum value of 2147483647 exceeded (got: 2147483648)"},
}

for _, v := range testVectors {
var unmarshaledPath BIP32Path
err := unmarshaledPath.UnmarshalText([]byte(v.strPath))
if !v.strPathValid {
require.EqualErrorf(
err,
v.errMsg,
"Unmarshaling invalid BIP-0032 string path: %s should fail with: %s",
v.strPath,
v.errMsg,
)
continue
}
require.NoErrorf(err, "Failed to unmarshal a valid BIP-0032 string path: %s", v.strPath)
require.Equal(v.path, unmarshaledPath, "Unmarshaled BIP-0032 path doesn't equal expected path")

textPath, err := unmarshaledPath.MarshalText()
require.NoError(err, "Failed to marshal a valid BIP-0032 path: %s", v.path)
require.Equal(v.strPath, string(textPath), "Marshaled BIP-0032 path doesn't equal expected text BIP-0032 path")
}
}

0 comments on commit 3e6c55b

Please sign in to comment.