diff --git a/cmd/common/wallet.go b/cmd/common/wallet.go index ffa24be3..67db5276 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,9 +78,20 @@ func LoadTestAccountConfig(name string) (*config.Account, error) { return nil, err } + kind := "" + if testAcc.SignatureAddressSpec().Ed25519 != nil { + 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 diff --git a/cmd/wallet/export.go b/cmd/wallet/export.go index 67381c12..494ca271 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/wallet/file/ed25519.go b/wallet/file/ed25519.go index dfb4b010..9d9680a7 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..955c29a2 --- /dev/null +++ b/wallet/file/ed25519_test.go @@ -0,0 +1,35 @@ +package file + +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..ebc1fadc 100644 --- a/wallet/file/file.go +++ b/wallet/file/file.go @@ -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" @@ -480,7 +479,7 @@ 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) } @@ -488,7 +487,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 +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) } @@ -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) } @@ -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() { diff --git a/wallet/file/secp256k1.go b/wallet/file/secp256k1.go index 79fa1d80..d9193638 100644 --- a/wallet/file/secp256k1.go +++ b/wallet/file/secp256k1.go @@ -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. diff --git a/wallet/file/secp256k1_test.go b/wallet/file/secp256k1_test.go index 7d48d5b4..8af0cd5a 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..df5f39a5 100644 --- a/wallet/file/sr25519.go +++ b/wallet/file/sr25519.go @@ -17,10 +17,10 @@ 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, @@ -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..98a54c20 100644 --- a/wallet/file/sr25519_test.go +++ b/wallet/file/sr25519_test.go @@ -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..8a44eeaa 100644 --- a/wallet/ledger/ledger.go +++ b/wallet/ledger/ledger.go @@ -290,9 +290,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..2d3c6ca8 100644 --- a/wallet/test/test.go +++ b/wallet/test/test.go @@ -14,11 +14,6 @@ import ( "github.com/oasisprotocol/cli/wallet" ) -const ( - // Kind is the account kind for the test accounts. - Kind = "test" -) - type testAccount struct { testKey testing.TestKey } @@ -54,9 +49,9 @@ 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), "" } diff --git a/wallet/wallet.go b/wallet/wallet.go index b8576970..9b5daea9 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -130,8 +130,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 reveals the account's private key and mnemonic. + UnsafeExport() (string, string) } // Register registers a new account type.