Files
sui-go-sdk/signer/derive.go

133 lines
2.6 KiB
Go

package signer
import (
"bytes"
"crypto/ed25519"
"crypto/hmac"
"crypto/sha512"
"encoding/binary"
"errors"
"regexp"
"strconv"
"strings"
)
const (
FirstHardenedIndex = uint32(0x80000000)
seedModifier = "ed25519 seed"
)
var (
ErrInvalidPath = errors.New("invalid derivation path")
ErrNoPublicDerivation = errors.New("no public derivation for ed25519")
pathRegex = regexp.MustCompile(`^m(\/[0-9]+')+$`)
)
type Key struct {
Key []byte
ChainCode []byte
}
// DeriveForPath derives key for a path in BIP-44 format and a seed.
// Ed25119 derivation operated on hardened keys only.
func DeriveForPath(path string, seed []byte) (*Key, error) {
if !isValidPath(path) {
return nil, ErrInvalidPath
}
key, err := NewMasterKey(seed)
if err != nil {
return nil, err
}
segments := strings.Split(path, "/")
for _, segment := range segments[1:] {
i64, err := strconv.ParseUint(strings.TrimRight(segment, "'"), 10, 32)
if err != nil {
return nil, err
}
i := uint32(i64) + FirstHardenedIndex
key, err = key.Derive(i)
if err != nil {
return nil, err
}
}
return key, nil
}
// NewMasterKey generates a new master key from seed.
func NewMasterKey(seed []byte) (*Key, error) {
hash := hmac.New(sha512.New, []byte(seedModifier))
_, err := hash.Write(seed)
if err != nil {
return nil, err
}
sum := hash.Sum(nil)
key := &Key{
Key: sum[:32],
ChainCode: sum[32:],
}
return key, nil
}
func (k *Key) Derive(i uint32) (*Key, error) {
// no public derivation for ed25519
if i < FirstHardenedIndex {
return nil, ErrNoPublicDerivation
}
iBytes := make([]byte, 4)
binary.BigEndian.PutUint32(iBytes, i)
key := append([]byte{0x0}, k.Key...)
data := append(key, iBytes...)
hash := hmac.New(sha512.New, k.ChainCode)
_, err := hash.Write(data)
if err != nil {
return nil, err
}
sum := hash.Sum(nil)
newKey := &Key{
Key: sum[:32],
ChainCode: sum[32:],
}
return newKey, nil
}
// PublicKey returns public key for a derived private key.
func (k *Key) PublicKey() ([]byte, error) {
reader := bytes.NewReader(k.Key)
pub, _, err := ed25519.GenerateKey(reader)
if err != nil {
return nil, err
}
return pub[:], nil
}
// RawSeed returns raw seed bytes
func (k *Key) RawSeed() [32]byte {
var rawSeed [32]byte
copy(rawSeed[:], k.Key[:])
return rawSeed
}
func isValidPath(path string) bool {
if !pathRegex.MatchString(path) {
return false
}
// Check for overflows
segments := strings.Split(path, "/")
for _, segment := range segments[1:] {
_, err := strconv.ParseUint(strings.TrimRight(segment, "'"), 10, 32)
if err != nil {
return false
}
}
return true
}