From 3e6c55b18e205dd1216f703e30197aa344111dfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tadej=20Jane=C5=BE?= Date: Tue, 4 May 2021 14:48:34 +0200 Subject: [PATCH] WIP: go/common/crypto/sakg: Add BIP32Path type TODO: Add docstrings. --- go/common/crypto/sakg/bip32.go | 87 +++++++++++++++++++++++++++++ go/common/crypto/sakg/bip32_test.go | 56 +++++++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 go/common/crypto/sakg/bip32.go create mode 100644 go/common/crypto/sakg/bip32_test.go diff --git a/go/common/crypto/sakg/bip32.go b/go/common/crypto/sakg/bip32.go new file mode 100644 index 00000000000..ff857731429 --- /dev/null +++ b/go/common/crypto/sakg/bip32.go @@ -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 +} diff --git a/go/common/crypto/sakg/bip32_test.go b/go/common/crypto/sakg/bip32_test.go new file mode 100644 index 00000000000..1043cde4ca2 --- /dev/null +++ b/go/common/crypto/sakg/bip32_test.go @@ -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") + } +}