Skip to content

Commit

Permalink
wallet: Derive private key on wallet export
Browse files Browse the repository at this point in the history
  • Loading branch information
matevz committed Dec 21, 2023
1 parent d60ff9b commit 5e6237c
Show file tree
Hide file tree
Showing 13 changed files with 168 additions and 61 deletions.
37 changes: 28 additions & 9 deletions cmd/common/wallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,8 @@ func LoadAccount(cfg *config.Config, name string) wallet.Account {
return acc
}

// Early check for whether the account exists so that we don't ask for passphrase first.
var (
acfg *config.Account
exists bool
)
if acfg, exists = cfg.Wallet.All[name]; !exists {
cobra.CheckErr(fmt.Errorf("account '%s' does not exist in the wallet", name))
}
acfg, err := LoadAccountConfig(cfg, name)
cobra.CheckErr(err)

af, err := acfg.LoadFactory()
cobra.CheckErr(err)
Expand All @@ -55,6 +49,20 @@ func LoadAccount(cfg *config.Config, name string) wallet.Account {
return acc
}

// LoadAccountConfig loads the config instance of the given named account.
func LoadAccountConfig(cfg *config.Config, name string) (*config.Account, error) {
if testName := helpers.ParseTestAccountAddress(name); testName != "" {
return LoadTestAccountConfig(testName)
}

// Early check for whether the account exists so that we don't ask for passphrase first.
if acfg, exists := cfg.Wallet.All[name]; exists {
return acfg, nil
}

return nil, fmt.Errorf("account '%s' does not exist in the wallet", name)
}

// LoadTestAccount loads the given named test account.
func LoadTestAccount(name string) (wallet.Account, error) {
if testKey, ok := testing.TestAccounts[name]; ok {
Expand All @@ -70,9 +78,20 @@ func LoadTestAccountConfig(name string) (*config.Account, error) {
return nil, err
}

kind := ""
if testAcc.SignatureAddressSpec().Ed25519 != nil {

Check failure on line 82 in cmd/common/wallet.go

View workflow job for this annotation

GitHub Actions / lint

ifElseChain: rewrite if-else to switch statement (gocritic)
kind = wallet.AlgorithmEd25519Raw
} else if testAcc.SignatureAddressSpec().Secp256k1Eth != nil {
kind = wallet.AlgorithmSecp256k1Raw
} else if testAcc.SignatureAddressSpec().Sr25519 != nil {
kind = wallet.AlgorithmSr25519Raw
} else {
return nil, fmt.Errorf("unrecognized kind for account %s", name)
}

return &config.Account{
Description: "",
Kind: test.Kind,
Kind: kind,
Address: testAcc.Address().String(),
Config: nil,
}, nil
Expand Down
18 changes: 15 additions & 3 deletions cmd/wallet/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,22 @@ var exportCmd = &cobra.Command{

fmt.Printf("WARNING: Exporting the account will expose secret key material!\n")
acc := common.LoadAccount(config.Global(), name)
accCfg, _ := common.LoadAccountConfig(config.Global(), name)

showPublicWalletInfo(name, acc)
showPublicWalletInfo(name, acc, accCfg)

fmt.Printf("Export:\n")
fmt.Println(acc.UnsafeExport())
key, mnemonic := acc.UnsafeExport()
if mnemonic != "" {
fmt.Printf("Secret mnemonic:\n")
fmt.Println(mnemonic)
if key != "" {
fmt.Printf("Derived secret key for account number '%d':\n", accCfg.Config["number"])
fmt.Println(key)
}
}
if mnemonic == "" && key != "" {
fmt.Printf("Secret key:\n")
fmt.Println(key)
}
},
}
11 changes: 9 additions & 2 deletions cmd/wallet/show.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,19 @@ var showCmd = &cobra.Command{
name := args[0]

acc := common.LoadAccount(config.Global(), name)
showPublicWalletInfo(name, acc)
accCfg, _ := common.LoadAccountConfig(config.Global(), name)
showPublicWalletInfo(name, acc, accCfg)
},
}

func showPublicWalletInfo(name string, wallet wallet.Account) {
func showPublicWalletInfo(name string, wallet wallet.Account, accCfg *config.Account) {
kind := "<unknown>"
if accCfg != nil {
kind = accCfg.PrettyKind()
}

fmt.Printf("Name: %s\n", name)
fmt.Printf("Kind: %s\n", kind)
if signer := wallet.Signer(); signer != nil {
fmt.Printf("Public Key: %s\n", signer.Public())
}
Expand Down
14 changes: 14 additions & 0 deletions wallet/file/ed25519.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,25 @@ package file

import (
"encoding/base64"
"fmt"

"github.com/oasisprotocol/curve25519-voi/primitives/ed25519"
"github.com/oasisprotocol/oasis-core/go/common/crypto/sakg"
"github.com/oasisprotocol/oasis-core/go/common/crypto/signature"
sdkSignature "github.com/oasisprotocol/oasis-sdk/client-sdk/go/crypto/signature"
ed255192 "github.com/oasisprotocol/oasis-sdk/client-sdk/go/crypto/signature/ed25519"
)

// ed25519FromMnemonic derives a signer using ADR-8 from given mnemonic.
func ed25519FromMnemonic(mnemonic string, number uint32) (sdkSignature.Signer, []byte, error) {
signer, _, err := sakg.GetAccountSigner(mnemonic, "", number)
if err != nil {
return nil, nil, fmt.Errorf("failed to derive key from mnemonic: %w", err)
}

return ed255192.WrapSigner(signer), signer.(signature.UnsafeSigner).UnsafeBytes(), nil
}

// ed25519rawSigner is an in-memory signer that allows deserialization of raw ed25519 keys for use
// in imported accounts that don't use ADR 0008.
type ed25519rawSigner struct {
Expand Down
35 changes: 35 additions & 0 deletions wallet/file/ed25519_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package file

Check failure on line 1 in wallet/file/ed25519_test.go

View workflow job for this annotation

GitHub Actions / lint

1-35 lines are duplicate of `wallet/file/sr25519_test.go:1-35` (dupl)

import (
"testing"

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

func TestEd25519FromMnemonic(t *testing.T) {
mnemonics := []struct {
mnemonic string
num uint32
pubkey string
valid bool
}{
{mnemonic: "equip will roof matter pink blind book anxiety banner elbow sun young", num: 0, pubkey: "RWAfdhrxfbpQJDUp5ilzLxxY0I/92qhJEjhUBHVynYU=", valid: true},
{mnemonic: "equip will roof matter pink blind book anxiety banner elbow sun young", num: 1, pubkey: "J+0Eo8Dc7GWRwAHk6jB9ZcvXEsuQ2Fq3cDw17uB6d90=", valid: true},
{mnemonic: "equip will roof matter pink blind book anxiety banner elbow sun young", num: 2, pubkey: "GUVqPwzz9MxebOUt71fZK7PFplH6liayRs/sB6vChyQ=", valid: true},
{mnemonic: "equip will roof matter pink blind book anxiety banner elbow sun young", num: 3, pubkey: "klSQRiFP20cpv3pu5KO70PRjxHasyTOyx8zghFCavuQ=", valid: true},
{mnemonic: "actorr want explain gravity body drill bike update mask wool tell seven", pubkey: "", valid: false},
{mnemonic: "actor want explain gravity body drill bike update mask wool tell", pubkey: "", valid: false},
{mnemonic: "", pubkey: "", valid: false},
}

for _, m := range mnemonics {
if m.valid {
signer, _, err := ed25519FromMnemonic(m.mnemonic, m.num)
require.NoError(t, err)
require.Equal(t, m.pubkey, signer.Public().String())
} else {
_, _, err := ed25519FromMnemonic(m.mnemonic, 0)
require.Error(t, err)
}
}
}
34 changes: 27 additions & 7 deletions wallet/file/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import (
"golang.org/x/crypto/sha3"

"github.com/oasisprotocol/deoxysii"
"github.com/oasisprotocol/oasis-core/go/common/crypto/sakg"
coreSignature "github.com/oasisprotocol/oasis-core/go/common/crypto/signature"
"github.com/oasisprotocol/oasis-sdk/client-sdk/go/crypto/signature"
"github.com/oasisprotocol/oasis-sdk/client-sdk/go/crypto/signature/ed25519"
Expand Down Expand Up @@ -480,15 +479,15 @@ func newAccount(state *secretState, cfg *accountConfig) (wallet.Account, error)
switch state.Algorithm {
case wallet.AlgorithmEd25519Adr8:
// For Ed25519 use the ADR 0008 derivation scheme.
signer, _, err := sakg.GetAccountSigner(state.Data, "", cfg.Number)
signer, _, err := ed25519FromMnemonic(state.Data, cfg.Number)
if err != nil {
return nil, fmt.Errorf("failed to derive signer: %w", err)
}

return &fileAccount{
cfg: cfg,
state: state,
signer: ed25519.WrapSigner(signer),
signer: signer,
}, nil
case wallet.AlgorithmEd25519Raw:
// For Ed25519-Raw use the raw private key.
Expand All @@ -504,7 +503,7 @@ func newAccount(state *secretState, cfg *accountConfig) (wallet.Account, error)
}, nil
case wallet.AlgorithmSecp256k1Bip44:
// For Secp256k1-BIP-44 use the BIP-44 derivation scheme.
signer, err := Secp256k1FromMnemonic(state.Data, cfg.Number)
signer, _, err := secp256k1FromMnemonic(state.Data, cfg.Number)
if err != nil {
return nil, fmt.Errorf("failed to initialize signer: %w", err)
}
Expand All @@ -527,7 +526,7 @@ func newAccount(state *secretState, cfg *accountConfig) (wallet.Account, error)
}, nil
case wallet.AlgorithmSr25519Adr8:
// For Sr25519 use the ADR 0008 derivation scheme.
signer, err := Sr25519FromMnemonic(state.Data, cfg.Number)
signer, _, err := sr25519FromMnemonic(state.Data, cfg.Number)
if err != nil {
return nil, fmt.Errorf("failed to initialize signer: %w", err)
}
Expand Down Expand Up @@ -604,8 +603,29 @@ func (a *fileAccount) SignatureAddressSpec() types.SignatureAddressSpec {
}
}

func (a *fileAccount) UnsafeExport() string {
return a.state.Data
func (a *fileAccount) UnsafeExport() (string, string) {
switch a.cfg.Algorithm {
case wallet.AlgorithmEd25519Raw, wallet.AlgorithmSecp256k1Raw, wallet.AlgorithmSr25519Raw:
return a.state.Data, ""
}

mnemonic := a.state.Data

// For convenience derive the corresponding private key of the mnemonic.
key := ""
switch a.cfg.Algorithm {
case wallet.AlgorithmEd25519Adr8:
_, sk, _ := ed25519FromMnemonic(a.state.Data, a.cfg.Number)
key = base64.StdEncoding.EncodeToString(sk)
case wallet.AlgorithmSecp256k1Bip44:
_, sk, _ := secp256k1FromMnemonic(a.state.Data, a.cfg.Number)
key = hex.EncodeToString(sk)
case wallet.AlgorithmSr25519Adr8:
_, sk, _ := sr25519FromMnemonic(a.state.Data, a.cfg.Number)
key = base64.StdEncoding.EncodeToString(sk)
}

return key, mnemonic
}

func init() {
Expand Down
12 changes: 6 additions & 6 deletions wallet/file/secp256k1.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,22 @@ const (
Bip44DerivationPath = "m/44'/60'/0'/0/%d"
)

// Secp256k1FromMnemonic derives a signer using BIP-44 from given mnemonic.
func Secp256k1FromMnemonic(mnemonic string, number uint32) (sdkSignature.Signer, error) {
// secp256k1FromMnemonic derives a signer using BIP-44 from given mnemonic.
func secp256k1FromMnemonic(mnemonic string, number uint32) (sdkSignature.Signer, []byte, error) {
wallet, err := hdwallet.NewFromMnemonic(mnemonic)
if err != nil {
return nil, fmt.Errorf("failed to parse mnemonic: %w", err)
return nil, nil, fmt.Errorf("failed to parse mnemonic: %w", err)
}
path := hdwallet.MustParseDerivationPath(fmt.Sprintf(Bip44DerivationPath, number))
account, err := wallet.Derive(path, false)
if err != nil {
return nil, fmt.Errorf("failed to derive key from mnemonic: %w", err)
return nil, nil, fmt.Errorf("failed to derive key from mnemonic: %w", err)
}
pk, err := wallet.PrivateKeyBytes(account)
if err != nil {
return nil, fmt.Errorf("failed to obtain generated private key: %w", err)
return nil, nil, fmt.Errorf("failed to obtain generated private key: %w", err)
}
return secp256k1.NewSigner(pk), nil
return secp256k1.NewSigner(pk), pk, nil
}

// Secp256k1FromHex creates a signer from given hex-encoded private key.
Expand Down
4 changes: 2 additions & 2 deletions wallet/file/secp256k1_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,11 @@ var mnemonics = []struct {
func TestSecp256k1FromMnemonic(t *testing.T) {
for _, m := range mnemonics {
if m.valid {
signer, err := Secp256k1FromMnemonic(m.mnemonic, m.num)
signer, _, err := secp256k1FromMnemonic(m.mnemonic, m.num)
require.NoError(t, err)
require.Equal(t, m.pubkey, signer.Public().String())
} else {
_, err := Secp256k1FromMnemonic(m.mnemonic, 0)
_, _, err := secp256k1FromMnemonic(m.mnemonic, 0)
require.Error(t, err)
}
}
Expand Down
41 changes: 23 additions & 18 deletions wallet/file/sr25519.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,48 +17,48 @@ import (
sdkSr25519 "github.com/oasisprotocol/oasis-sdk/client-sdk/go/crypto/signature/sr25519"
)

// Sr25519FromMnemonic derives a signer using ADR-8 from given mnemonic.
func Sr25519FromMnemonic(mnemonic string, number uint32) (sdkSignature.Signer, error) {
// sr25519FromMnemonic derives a signer using ADR-8 from given mnemonic.
func sr25519FromMnemonic(mnemonic string, number uint32) (sdkSignature.Signer, []byte, error) {
if number > sakg.MaxAccountKeyNumber {
return nil, fmt.Errorf(
return nil, nil, fmt.Errorf(
"sakg: invalid key number: %d (maximum: %d)",
number,
sakg.MaxAccountKeyNumber,
)
}

if !bip39.IsMnemonicValid(mnemonic) {
return nil, fmt.Errorf("sakg: invalid mnemonic")
return nil, nil, fmt.Errorf("sakg: invalid mnemonic")
}

seed := bip39.NewSeed(mnemonic, "")

_, chainCode, skBinary, err := newMasterKey(seed)
_, chainCode, skBinary, skCanBinary, err := newMasterKey(seed)
if err != nil {
return nil, fmt.Errorf("sakg: error deriving master key: %w", err)
return nil, nil, fmt.Errorf("sakg: error deriving master key: %w", err)
}

pathStr := fmt.Sprintf("%s/%d'", sakg.BIP32PathPrefix, number)
path, err := sakg.NewBIP32Path(pathStr)
if err != nil {
return nil, fmt.Errorf("sakg: error creating BIP-0032 path %s: %w", pathStr, err)
return nil, nil, fmt.Errorf("sakg: error creating BIP-0032 path %s: %w", pathStr, err)
}

var signer sdkSignature.Signer
for _, index := range path {
signer, chainCode, skBinary, err = newChildKey(skBinary, chainCode, index)
signer, chainCode, skBinary, skCanBinary, err = newChildKey(skBinary, chainCode, index)
if err != nil {
return nil, fmt.Errorf("sakg: error deriving child key: %w", err)
return nil, nil, fmt.Errorf("sakg: error deriving child key: %w", err)
}
}

return signer, nil
return signer, skCanBinary, nil
}

func newMasterKey(seed []byte) (sdkSignature.Signer, slip10.ChainCode, []byte, error) {
func newMasterKey(seed []byte) (sdkSignature.Signer, slip10.ChainCode, []byte, []byte, error) {
// Let S be a seed byte sequence of 128 to 512 bits in length.
if sLen := len(seed); sLen < slip10.SeedMinSize || sLen > slip10.SeedMaxSize {
return nil, slip10.ChainCode{}, nil, fmt.Errorf("slip10: invalid seed")
return nil, slip10.ChainCode{}, nil, nil, fmt.Errorf("slip10: invalid seed")
}

// 1. Calculate I = HMAC-SHA512(Key = Curve, Data = S)
Expand All @@ -71,16 +71,16 @@ func newMasterKey(seed []byte) (sdkSignature.Signer, slip10.ChainCode, []byte, e
return splitDigest(I)
}

func newChildKey(kPar []byte, cPar slip10.ChainCode, index uint32) (sdkSignature.Signer, slip10.ChainCode, []byte, error) {
func newChildKey(kPar []byte, cPar slip10.ChainCode, index uint32) (sdkSignature.Signer, slip10.ChainCode, []byte, []byte, error) {
if len(kPar) < memory.SeedSize {
return nil, slip10.ChainCode{}, nil, fmt.Errorf("slip10: invalid parent key")
return nil, slip10.ChainCode{}, nil, nil, fmt.Errorf("slip10: invalid parent key")
}

// 1. Check whether i >= 2^31 (whether the child is a hardened key).
if index < 1<<31 {
// If not (normal child):
// If curve is ed25519: return failure.
return nil, slip10.ChainCode{}, nil, fmt.Errorf("slip10: non-hardened keys not supported")
return nil, slip10.ChainCode{}, nil, nil, fmt.Errorf("slip10: non-hardened keys not supported")
}

// If so (hardened child):
Expand All @@ -100,20 +100,25 @@ func newChildKey(kPar []byte, cPar slip10.ChainCode, index uint32) (sdkSignature
return splitDigest(I)
}

func splitDigest(digest []byte) (sdkSignature.Signer, slip10.ChainCode, []byte, error) {
func splitDigest(digest []byte) (sdkSignature.Signer, slip10.ChainCode, []byte, []byte, error) {
IL, IR := digest[:32], digest[32:]

var chainCode slip10.ChainCode

edSk := ed25519.NewKeyFromSeed(IL) // Needed for the SLIP10 scheme.
msk, err := sr25519.NewMiniSecretKeyFromBytes(IL)
if err != nil {
return nil, chainCode, nil, err
return nil, chainCode, nil, nil, err
}
sk := msk.ExpandUniform()

signer := sdkSr25519.NewSignerFromKeyPair(sk.KeyPair())
copy(chainCode[:], IR)

return signer, chainCode, edSk[:], nil
skCanBinary, err := sk.MarshalBinary()
if err != nil {
return nil, chainCode, nil, nil, err
}

return signer, chainCode, edSk[:], skCanBinary, nil
}
Loading

0 comments on commit 5e6237c

Please sign in to comment.