From e0f3f92bba9a32fc78913b082683c41356cf2e39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matev=C5=BE=20Jekovec?= Date: Mon, 2 Oct 2023 13:17:00 +0200 Subject: [PATCH] wallet: Derive private key on wallet export --- cmd/common/wallet.go | 38 ++++++-- cmd/wallet/export.go | 18 +++- cmd/wallet/show.go | 11 ++- examples/wallet/export-ledger.out.static | 2 +- .../wallet/export-secp256k1-bip44.out.static | 5 +- .../wallet/export-secp256k1-raw.out.static | 3 +- examples/wallet/export.out.static | 5 +- examples/wallet/show-ledger.out.static | 1 + examples/wallet/show-secp256k1.out.static | 1 + examples/wallet/show.out.static | 1 + wallet/file/ed25519.go | 14 +++ wallet/file/ed25519_test.go | 35 +++++++ wallet/file/file.go | 90 +++++++++--------- wallet/file/secp256k1.go | 12 +-- wallet/file/secp256k1_test.go | 4 +- wallet/file/sr25519.go | 39 ++++---- wallet/file/sr25519_test.go | 6 +- wallet/ledger/ledger.go | 46 +++------- wallet/test/test.go | 91 ++++++++++++++++++- wallet/wallet.go | 24 ++++- 20 files changed, 318 insertions(+), 128 deletions(-) create mode 100644 wallet/file/ed25519_test.go diff --git a/cmd/common/wallet.go b/cmd/common/wallet.go index ffa24be3..ed6c3056 100644 --- a/cmd/common/wallet.go +++ b/cmd/common/wallet.go @@ -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) @@ -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 { @@ -70,11 +78,23 @@ func LoadTestAccountConfig(name string) (*config.Account, error) { return nil, err } + alg := "" + switch { + case testAcc.SignatureAddressSpec().Ed25519 != nil: + alg = wallet.AlgorithmEd25519Raw + case testAcc.SignatureAddressSpec().Secp256k1Eth != nil: + alg = wallet.AlgorithmSecp256k1Raw + case testAcc.SignatureAddressSpec().Sr25519 != nil: + alg = wallet.AlgorithmSr25519Raw + default: + return nil, fmt.Errorf("unrecognized algorithm for test account %s", name) + } + return &config.Account{ Description: "", Kind: test.Kind, Address: testAcc.Address().String(), - Config: nil, + Config: map[string]interface{}{"algorithm": alg}, }, nil } diff --git a/cmd/wallet/export.go b/cmd/wallet/export.go index 67381c12..3abee51e 100644 --- a/cmd/wallet/export.go +++ b/cmd/wallet/export.go @@ -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) + } }, } diff --git a/cmd/wallet/show.go b/cmd/wallet/show.go index 6ce45335..d1994a57 100644 --- a/cmd/wallet/show.go +++ b/cmd/wallet/show.go @@ -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 := "" + 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()) } diff --git a/examples/wallet/export-ledger.out.static b/examples/wallet/export-ledger.out.static index caea467f..ff552116 100644 --- a/examples/wallet/export-ledger.out.static +++ b/examples/wallet/export-ledger.out.static @@ -1,6 +1,6 @@ WARNING: Exporting the account will expose secret key material! Name: lenny +Kind: ledger (secp256k1-bip44:3) Public Key: AhhT2TUkEZ7rMasLBvHcsGj4SUO7Iw36ELEpL0evZDV1 Ethereum address: 0x95e5e3C1BDD92cd4A0c14c62480DB5867946281D Native address: oasis1qrmw4rhvp8ksj3yx6p2ftnkz864muc3re5jlgall -Export: diff --git a/examples/wallet/export-secp256k1-bip44.out.static b/examples/wallet/export-secp256k1-bip44.out.static index 1dc8580b..7da0be2d 100644 --- a/examples/wallet/export-secp256k1-bip44.out.static +++ b/examples/wallet/export-secp256k1-bip44.out.static @@ -2,8 +2,11 @@ WARNING: Exporting the account will expose secret key material! Unlock your account. ? Passphrase: Name: eugene +Kind: file (secp256k1-bip44:0) Public Key: ArEjDxsPfDvfeLlity4mjGzy8E/nI4umiC8vYQh+eh/c Ethereum address: 0xBd16C6bF701a01DF1B5C11B14860b6bDbE776669 Native address: oasis1qrvzxld9rz83wv92lvnkpmr30c77kj2tvg0pednz -Export: +Secret mnemonic: man ankle mystery favorite tone number ice west spare marriage control lucky life together neither +Derived secret key for account number 0: +c559cad1e71e0db1b3a657f47ca7a618bfb6a51a7294df72bcfca57aded5377e diff --git a/examples/wallet/export-secp256k1-raw.out.static b/examples/wallet/export-secp256k1-raw.out.static index a5a6a23e..eb1435b4 100644 --- a/examples/wallet/export-secp256k1-raw.out.static +++ b/examples/wallet/export-secp256k1-raw.out.static @@ -2,8 +2,9 @@ WARNING: Exporting the account will expose secret key material! Unlock your account. ? Passphrase: Name: emma +Kind: file (secp256k1-raw) Public Key: Az8B2UpSUET0E3n9XMzr+HBvviQKcRvz6C6bJtRFWNYG Ethereum address: 0xeEbE22411f579682F6f9D68f4C19B3581bCb576b Native address: oasis1qph93wnfw8shu04pqyarvtjy4lytz3hp0c7tqnqh -Export: +Secret key: 4811ebbe4f29f32a758f6f7bad39deb97ea67f07350637e31c75795dc679262a diff --git a/examples/wallet/export.out.static b/examples/wallet/export.out.static index d689c863..0d1fccc0 100644 --- a/examples/wallet/export.out.static +++ b/examples/wallet/export.out.static @@ -2,7 +2,10 @@ WARNING: Exporting the account will expose secret key material! Unlock your account. ? Passphrase: Name: oscar +Kind: file (ed25519-adr8:0) Public Key: Bx6gOixnxy15tCs09ua5DcKyX9uo2Forb32O6Hyjoc8= Native address: oasis1qp87hflmelnpqhzcqcw8rhzakq4elj7jzv090p3e -Export: +Secret mnemonic: promote easily runway junior saddle gold flip believe wet example amount believe habit mixed pistol lemon increase moon rail mail fiction miss clip asset +Derived secret key for account number 0: +LHOUUJgVquTdi/3DVsS4caW4jQcvuFgl1Oag6BwlNvwHHqA6LGfHLXm0KzT25rkNwrJf26jYWitvfY7ofKOhzw== diff --git a/examples/wallet/show-ledger.out.static b/examples/wallet/show-ledger.out.static index dd6cea11..d3451339 100644 --- a/examples/wallet/show-ledger.out.static +++ b/examples/wallet/show-ledger.out.static @@ -1,3 +1,4 @@ Name: logan +Kind: ledger (ed25519-legacy:0) Public Key: l+cuboPsOeuY1+kYlROrpmKgiiELmXSw9xl0WEg8cWE= Native address: oasis1qpl4axynedmdrrgrg7dpw3yxc4a8crevr5dkuksl diff --git a/examples/wallet/show-secp256k1.out.static b/examples/wallet/show-secp256k1.out.static index 9e6c60ec..8674e8a7 100644 --- a/examples/wallet/show-secp256k1.out.static +++ b/examples/wallet/show-secp256k1.out.static @@ -1,6 +1,7 @@ Unlock your account. ? Passphrase: Name: eugene +Kind: file (secp256k1-bip44:0) Public Key: ArEjDxsPfDvfeLlity4mjGzy8E/nI4umiC8vYQh+eh/c Ethereum address: 0xBd16C6bF701a01DF1B5C11B14860b6bDbE776669 Native address: oasis1qrvzxld9rz83wv92lvnkpmr30c77kj2tvg0pednz diff --git a/examples/wallet/show.out.static b/examples/wallet/show.out.static index 34759dec..d3c791f0 100644 --- a/examples/wallet/show.out.static +++ b/examples/wallet/show.out.static @@ -1,5 +1,6 @@ Unlock your account. ? Passphrase: Name: oscar +Kind: file (ed25519-adr8:0) Public Key: Bx6gOixnxy15tCs09ua5DcKyX9uo2Forb32O6Hyjoc8= Native address: oasis1qp87hflmelnpqhzcqcw8rhzakq4elj7jzv090p3e diff --git a/wallet/file/ed25519.go b/wallet/file/ed25519.go index dfb4b010..9be69bc4 100644 --- a/wallet/file/ed25519.go +++ b/wallet/file/ed25519.go @@ -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 { diff --git a/wallet/file/ed25519_test.go b/wallet/file/ed25519_test.go new file mode 100644 index 00000000..947ef4ec --- /dev/null +++ b/wallet/file/ed25519_test.go @@ -0,0 +1,35 @@ +package file //nolint: 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) + } + } +} diff --git a/wallet/file/file.go b/wallet/file/file.go index 6eea1e7c..bcd96d18 100644 --- a/wallet/file/file.go +++ b/wallet/file/file.go @@ -11,14 +11,12 @@ import ( "github.com/AlecAivazis/survey/v2" ethCommon "github.com/ethereum/go-ethereum/common" - "github.com/mitchellh/mapstructure" flag "github.com/spf13/pflag" bip39 "github.com/tyler-smith/go-bip39" "golang.org/x/crypto/argon2" "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" @@ -58,11 +56,6 @@ func SupportedAlgorithmsForImport(kind *wallet.ImportKind) []string { } } -type accountConfig struct { - Algorithm string `mapstructure:"algorithm"` - Number uint32 `mapstructure:"number,omitempty"` -} - type secretState struct { // Algorithm is the cryptographic algorithm used by the account. Algorithm string `json:"algorithm"` @@ -184,8 +177,8 @@ func (af *fileAccountFactory) Kind() string { } func (af *fileAccountFactory) PrettyKind(rawCfg map[string]interface{}) string { - cfg, err := af.unmarshalConfig(rawCfg) - if err != nil { + var cfg wallet.AccountConfig + if err := cfg.UnmarshalMap(rawCfg); err != nil { return "" } @@ -249,8 +242,8 @@ func (af *fileAccountFactory) DataPrompt(kind wallet.ImportKind, rawCfg map[stri case wallet.ImportKindMnemonic: return &survey.Multiline{Message: "Mnemonic:"} case wallet.ImportKindPrivateKey: - cfg, err := af.unmarshalConfig(rawCfg) - if err != nil { + var cfg wallet.AccountConfig + if err := cfg.UnmarshalMap(rawCfg); err != nil { return nil } switch cfg.Algorithm { @@ -273,8 +266,8 @@ func (af *fileAccountFactory) DataValidator(kind wallet.ImportKind, rawCfg map[s switch kind { case wallet.ImportKindMnemonic: case wallet.ImportKindPrivateKey: - cfg, err := af.unmarshalConfig(rawCfg) - if err != nil { + var cfg wallet.AccountConfig + if err := cfg.UnmarshalMap(rawCfg); err != nil { return nil } switch cfg.Algorithm { @@ -319,8 +312,8 @@ func (af *fileAccountFactory) SupportedImportKinds() []wallet.ImportKind { } func (af *fileAccountFactory) HasConsensusSigner(rawCfg map[string]interface{}) bool { - cfg, err := af.unmarshalConfig(rawCfg) - if err != nil { + var cfg wallet.AccountConfig + if err := cfg.UnmarshalMap(rawCfg); err != nil { return false } @@ -331,21 +324,9 @@ func (af *fileAccountFactory) HasConsensusSigner(rawCfg map[string]interface{}) return false } -func (af *fileAccountFactory) unmarshalConfig(raw map[string]interface{}) (*accountConfig, error) { - if raw == nil { - return nil, fmt.Errorf("missing configuration") - } - - var cfg accountConfig - if err := mapstructure.Decode(raw, &cfg); err != nil { - return nil, err - } - return &cfg, nil -} - func (af *fileAccountFactory) Create(name string, passphrase string, rawCfg map[string]interface{}) (wallet.Account, error) { - cfg, err := af.unmarshalConfig(rawCfg) - if err != nil { + var cfg wallet.AccountConfig + if err := cfg.UnmarshalMap(rawCfg); err != nil { return nil, err } @@ -378,7 +359,7 @@ func (af *fileAccountFactory) Create(name string, passphrase string, rawCfg map[ } // Create a proper account based on the chosen algorithm. - return newAccount(state, cfg) + return newAccount(state, &cfg) } // Migrate migrates the given wallet config entry to the latest version and returns true, if any changes were needed. @@ -387,8 +368,8 @@ func (af *fileAccountFactory) Migrate(_ map[string]interface{}) bool { } func (af *fileAccountFactory) Load(name string, passphrase string, rawCfg map[string]interface{}) (wallet.Account, error) { - cfg, err := af.unmarshalConfig(rawCfg) - if err != nil { + var cfg wallet.AccountConfig + if err := cfg.UnmarshalMap(rawCfg); err != nil { return nil, err } @@ -408,7 +389,7 @@ func (af *fileAccountFactory) Load(name string, passphrase string, rawCfg map[st return nil, fmt.Errorf("failed to open account state (maybe incorrect passphrase?)") } - return newAccount(state, cfg) + return newAccount(state, &cfg) } func (af *fileAccountFactory) Remove(name string, _ map[string]interface{}) error { @@ -420,8 +401,8 @@ func (af *fileAccountFactory) Rename(old, new string, _ map[string]interface{}) } func (af *fileAccountFactory) Import(name string, passphrase string, rawCfg map[string]interface{}, src *wallet.ImportSource) (wallet.Account, error) { - cfg, err := af.unmarshalConfig(rawCfg) - if err != nil { + var cfg wallet.AccountConfig + if err := cfg.UnmarshalMap(rawCfg); err != nil { return nil, err } @@ -449,7 +430,7 @@ func (af *fileAccountFactory) Import(name string, passphrase string, rawCfg map[ } // Create a proper account based on the chosen algorithm. - acc, err := newAccount(&state, cfg) + acc, err := newAccount(&state, &cfg) if err != nil { return nil, err } @@ -471,16 +452,16 @@ func (af *fileAccountFactory) Import(name string, passphrase string, rawCfg map[ } type fileAccount struct { - cfg *accountConfig + cfg *wallet.AccountConfig state *secretState signer signature.Signer } -func newAccount(state *secretState, cfg *accountConfig) (wallet.Account, error) { +func newAccount(state *secretState, cfg *wallet.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) } @@ -488,7 +469,7 @@ func newAccount(state *secretState, cfg *accountConfig) (wallet.Account, error) return &fileAccount{ cfg: cfg, state: state, - signer: ed25519.WrapSigner(signer), + signer: signer, }, nil case wallet.AlgorithmEd25519Raw: // For Ed25519-Raw use the raw private key. @@ -504,7 +485,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) } @@ -527,7 +508,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) } @@ -604,8 +585,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() { diff --git a/wallet/file/secp256k1.go b/wallet/file/secp256k1.go index 79fa1d80..1961f6f0 100644 --- a/wallet/file/secp256k1.go +++ b/wallet/file/secp256k1.go @@ -20,21 +20,21 @@ const ( ) // Secp256k1FromMnemonic derives a signer using BIP-44 from given mnemonic. -func Secp256k1FromMnemonic(mnemonic string, number uint32) (sdkSignature.Signer, error) { +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) + sk, 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(sk), sk, nil } // Secp256k1FromHex creates a signer from given hex-encoded private key. diff --git a/wallet/file/secp256k1_test.go b/wallet/file/secp256k1_test.go index 7d48d5b4..975f8d47 100644 --- a/wallet/file/secp256k1_test.go +++ b/wallet/file/secp256k1_test.go @@ -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) } } diff --git a/wallet/file/sr25519.go b/wallet/file/sr25519.go index 00d3040a..da44e616 100644 --- a/wallet/file/sr25519.go +++ b/wallet/file/sr25519.go @@ -18,9 +18,9 @@ import ( ) // Sr25519FromMnemonic derives a signer using ADR-8 from given mnemonic. -func Sr25519FromMnemonic(mnemonic string, number uint32) (sdkSignature.Signer, error) { +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, @@ -28,37 +28,37 @@ func Sr25519FromMnemonic(mnemonic string, number uint32) (sdkSignature.Signer, e } 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) @@ -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): @@ -100,7 +100,7 @@ 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 @@ -108,12 +108,17 @@ func splitDigest(digest []byte) (sdkSignature.Signer, slip10.ChainCode, []byte, 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 } diff --git a/wallet/file/sr25519_test.go b/wallet/file/sr25519_test.go index 2851e2ce..d5b801d5 100644 --- a/wallet/file/sr25519_test.go +++ b/wallet/file/sr25519_test.go @@ -1,4 +1,4 @@ -package file +package file //nolint: dupl import ( "testing" @@ -24,11 +24,11 @@ func TestSr25519FromMnemonic(t *testing.T) { for _, m := range mnemonics { if m.valid { - signer, err := Sr25519FromMnemonic(m.mnemonic, m.num) + signer, _, err := Sr25519FromMnemonic(m.mnemonic, m.num) require.NoError(t, err) require.Equal(t, m.pubkey, signer.Public().String()) } else { - _, err := Sr25519FromMnemonic(m.mnemonic, 0) + _, _, err := Sr25519FromMnemonic(m.mnemonic, 0) require.Error(t, err) } } diff --git a/wallet/ledger/ledger.go b/wallet/ledger/ledger.go index 631c6e39..f03df821 100644 --- a/wallet/ledger/ledger.go +++ b/wallet/ledger/ledger.go @@ -5,7 +5,6 @@ import ( "github.com/AlecAivazis/survey/v2" ethCommon "github.com/ethereum/go-ethereum/common" - "github.com/mitchellh/mapstructure" flag "github.com/spf13/pflag" "golang.org/x/crypto/sha3" @@ -27,11 +26,6 @@ const ( cfgNumber = "ledger.number" ) -type accountConfig struct { - Algorithm string `mapstructure:"algorithm"` - Number uint32 `mapstructure:"number,omitempty"` -} - type ledgerAccountFactory struct { flags *flag.FlagSet } @@ -41,8 +35,8 @@ func (af *ledgerAccountFactory) Kind() string { } func (af *ledgerAccountFactory) PrettyKind(rawCfg map[string]interface{}) string { - cfg, err := af.unmarshalConfig(rawCfg) - if err != nil { + var cfg wallet.AccountConfig + if err := cfg.UnmarshalMap(rawCfg); err != nil { return "" } @@ -86,8 +80,8 @@ func (af *ledgerAccountFactory) SupportedImportKinds() []wallet.ImportKind { } func (af *ledgerAccountFactory) HasConsensusSigner(rawCfg map[string]interface{}) bool { - cfg, err := af.unmarshalConfig(rawCfg) - if err != nil { + var cfg wallet.AccountConfig + if err := cfg.UnmarshalMap(rawCfg); err != nil { return false } @@ -121,34 +115,22 @@ func (af *ledgerAccountFactory) Migrate(rawCfg map[string]interface{}) bool { return changed } -func (af *ledgerAccountFactory) unmarshalConfig(raw map[string]interface{}) (*accountConfig, error) { - if raw == nil { - return nil, fmt.Errorf("missing configuration") - } - - var cfg accountConfig - if err := mapstructure.Decode(raw, &cfg); err != nil { - return nil, err - } - return &cfg, nil -} - func (af *ledgerAccountFactory) Create(_ string, _ string, rawCfg map[string]interface{}) (wallet.Account, error) { - cfg, err := af.unmarshalConfig(rawCfg) - if err != nil { + var cfg wallet.AccountConfig + if err := cfg.UnmarshalMap(rawCfg); err != nil { return nil, err } - return newAccount(cfg) + return newAccount(&cfg) } func (af *ledgerAccountFactory) Load(_ string, _ string, rawCfg map[string]interface{}) (wallet.Account, error) { - cfg, err := af.unmarshalConfig(rawCfg) - if err != nil { + var cfg wallet.AccountConfig + if err := cfg.UnmarshalMap(rawCfg); err != nil { return nil, err } - return newAccount(cfg) + return newAccount(&cfg) } func (af *ledgerAccountFactory) Remove(_ string, _ map[string]interface{}) error { @@ -164,12 +146,12 @@ func (af *ledgerAccountFactory) Import(_ string, _ string, _ map[string]interfac } type ledgerAccount struct { - cfg *accountConfig + cfg *wallet.AccountConfig signer *ledgerSigner coreSigner *ledgerCoreSigner } -func newAccount(cfg *accountConfig) (wallet.Account, error) { +func newAccount(cfg *wallet.AccountConfig) (wallet.Account, error) { // Connect to device. dev, err := connectToDevice() if err != nil { @@ -290,9 +272,9 @@ func (a *ledgerAccount) SignatureAddressSpec() types.SignatureAddressSpec { return types.SignatureAddressSpec{} } -func (a *ledgerAccount) UnsafeExport() string { +func (a *ledgerAccount) UnsafeExport() (string, string) { // Secret is stored on the device. - return "" + return "", "" } func init() { diff --git a/wallet/test/test.go b/wallet/test/test.go index 2d1d418a..861aad5d 100644 --- a/wallet/test/test.go +++ b/wallet/test/test.go @@ -3,10 +3,13 @@ package test import ( "encoding/base64" "encoding/hex" + "fmt" + "github.com/AlecAivazis/survey/v2" ethCommon "github.com/ethereum/go-ethereum/common" - coreSignature "github.com/oasisprotocol/oasis-core/go/common/crypto/signature" + flag "github.com/spf13/pflag" + 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/testing" "github.com/oasisprotocol/oasis-sdk/client-sdk/go/types" @@ -19,6 +22,82 @@ const ( Kind = "test" ) +type testAccountFactory struct{} + +func (af *testAccountFactory) Kind() string { + return Kind +} + +func (af *testAccountFactory) PrettyKind(rawCfg map[string]interface{}) string { + var cfg wallet.AccountConfig + if err := cfg.UnmarshalMap(rawCfg); err != nil { + return "" + } + + return fmt.Sprintf("test (%s)", cfg.Algorithm) +} + +func (af *testAccountFactory) Flags() *flag.FlagSet { + return nil +} + +func (af *testAccountFactory) GetConfigFromFlags() (map[string]interface{}, error) { + return nil, nil +} + +func (af *testAccountFactory) GetConfigFromSurvey(_ *wallet.ImportKind) (map[string]interface{}, error) { + return nil, fmt.Errorf("test: account is built-in") +} + +func (af *testAccountFactory) DataPrompt(_ wallet.ImportKind, _ map[string]interface{}) survey.Prompt { + return nil +} + +func (af *testAccountFactory) DataValidator(_ wallet.ImportKind, _ map[string]interface{}) survey.Validator { + return nil +} + +func (af *testAccountFactory) RequiresPassphrase() bool { + return false +} + +func (af *testAccountFactory) SupportedImportKinds() []wallet.ImportKind { + return []wallet.ImportKind{} +} + +func (af *testAccountFactory) HasConsensusSigner(rawCfg map[string]interface{}) bool { + var cfg wallet.AccountConfig + if err := cfg.UnmarshalMap(rawCfg); err != nil { + return false + } + + return cfg.Algorithm == wallet.AlgorithmEd25519Raw +} + +func (af *testAccountFactory) Migrate(_ map[string]interface{}) bool { + return false +} + +func (af *testAccountFactory) Create(_ string, _ string, _ map[string]interface{}) (wallet.Account, error) { + return nil, fmt.Errorf("test: account is built-in") +} + +func (af *testAccountFactory) Load(_ string, _ string, _ map[string]interface{}) (wallet.Account, error) { + return nil, fmt.Errorf("test: account is built-in") +} + +func (af *testAccountFactory) Remove(_ string, _ map[string]interface{}) error { + return fmt.Errorf("test: account is built-in") +} + +func (af *testAccountFactory) Rename(_, _ string, _ map[string]interface{}) error { + return fmt.Errorf("test: account is built-in") +} + +func (af *testAccountFactory) Import(_ string, _ string, _ map[string]interface{}, _ *wallet.ImportSource) (wallet.Account, error) { + return nil, fmt.Errorf("test: import not supported") +} + type testAccount struct { testKey testing.TestKey } @@ -54,9 +133,13 @@ func (a *testAccount) SignatureAddressSpec() types.SignatureAddressSpec { return a.testKey.SigSpec } -func (a *testAccount) UnsafeExport() string { +func (a *testAccount) UnsafeExport() (string, string) { if a.testKey.SigSpec.Secp256k1Eth != nil { - return hex.EncodeToString(a.testKey.SecretKey) + return hex.EncodeToString(a.testKey.SecretKey), "" } - return base64.StdEncoding.EncodeToString(a.testKey.SecretKey) + return base64.StdEncoding.EncodeToString(a.testKey.SecretKey), "" +} + +func init() { + wallet.Register(&testAccountFactory{}) } diff --git a/wallet/wallet.go b/wallet/wallet.go index b8576970..58b8d412 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -6,6 +6,7 @@ import ( "github.com/AlecAivazis/survey/v2" ethCommon "github.com/ethereum/go-ethereum/common" + "github.com/mitchellh/mapstructure" flag "github.com/spf13/pflag" coreSignature "github.com/oasisprotocol/oasis-core/go/common/crypto/signature" @@ -32,6 +33,25 @@ const ( AlgorithmSr25519Raw = "sr25519-raw" ) +// AccountConfig is an in-memory version of the account config stored in Oasis +// CLI config file. +type AccountConfig struct { + Algorithm string `mapstructure:"algorithm"` + Number uint32 `mapstructure:"number,omitempty"` +} + +// UnmarshalMap imports the config map to AccountConfig. +func (af *AccountConfig) UnmarshalMap(raw map[string]interface{}) error { + if raw == nil { + return fmt.Errorf("missing configuration") + } + + if err := mapstructure.Decode(raw, &af); err != nil { + return err + } + return nil +} + // Factory is a factory that supports accounts of a specific kind. type Factory interface { // Kind returns the kind of accounts this factory will produce. @@ -130,8 +150,8 @@ type Account interface { // SignatureAddressSpec returns the signature address specification associated with the account. SignatureAddressSpec() types.SignatureAddressSpec - // UnsafeExport exports the account's secret state. - UnsafeExport() string + // UnsafeExport returns the account's private key and mnemonic. + UnsafeExport() (string, string) } // Register registers a new account type.