diff --git a/internal/rpchelp/helpdescs_en_US.go b/internal/rpchelp/helpdescs_en_US.go index e06536e6e..c728044b5 100644 --- a/internal/rpchelp/helpdescs_en_US.go +++ b/internal/rpchelp/helpdescs_en_US.go @@ -538,6 +538,15 @@ var helpDescsEnUS = map[string]string{ "verifymessage-message": "The message to verify", "verifymessage--result0": "Whether the message was signed with the private key of 'address'", + // VerifySeedCmd help. + "verifyseed--synopsis": "Verifys if a seed is the same as the running wallet.", + "verifyseed-seed": "Seed to be inputted to check against the wallets master public key, after derivation.", + "verifyseed-account": "Account number, of potential branch of the master public key, this is an optional input.", + + // VerifySeedResults help. + "verifyseedresult-keyresult": "Whether or not if the inputted seed is the same as the running wallets", + "verifyseedresult-cointype": "Outputs the current cointype of the running wallet.", + // Version help "version--synopsis": "Returns application and API versions (semver) keyed by their names", "version--result0--desc": "Version objects keyed by the program or API name", diff --git a/internal/rpchelp/methods.go b/internal/rpchelp/methods.go index fa05010e4..d0f3e7ad8 100644 --- a/internal/rpchelp/methods.go +++ b/internal/rpchelp/methods.go @@ -76,6 +76,7 @@ var Methods = []struct { {"sweepaccount", []interface{}{(*dcrjson.SweepAccountResult)(nil)}}, {"validateaddress", []interface{}{(*dcrjson.ValidateAddressWalletResult)(nil)}}, {"verifymessage", returnsBool}, + {"verifyseed", []interface{}{(*dcrjson.VerifySeedResult)(nil)}}, {"version", []interface{}{(*map[string]dcrjson.VersionResult)(nil)}}, {"walletlock", nil}, {"walletpassphrase", nil}, diff --git a/rpc/legacyrpc/methods.go b/rpc/legacyrpc/methods.go index aeff3c096..eb5c3a72a 100644 --- a/rpc/legacyrpc/methods.go +++ b/rpc/legacyrpc/methods.go @@ -32,6 +32,7 @@ import ( "github.com/decred/dcrwallet/wallet/txauthor" "github.com/decred/dcrwallet/wallet/txrules" "github.com/decred/dcrwallet/wallet/udb" + "github.com/decred/dcrwallet/walletseed" ) // API version constants @@ -119,6 +120,7 @@ var handlers = map[string]handler{ "ticketsforaddress": {fn: ticketsForAddress}, "validateaddress": {fn: validateAddress}, "verifymessage": {fn: verifyMessage}, + "verifyseed": {fn: verifySeed}, "version": {fn: version}, "walletinfo": {fn: walletInfo}, "walletlock": {fn: walletLock}, @@ -3409,6 +3411,107 @@ WrongAddrKind: return nil, rpcErrorf(dcrjson.ErrRPCInvalidParameter, "address must be secp256k1 P2PK or P2PKH") } +func deriveCoinTypeKey(seed []byte, coinType uint32, params *chaincfg.Params) (*hdkeychain.ExtendedKey, error) { + // Create new root from the inputted seed and the current net + root, err := hdkeychain.NewMaster(seed[:], params) + if err != nil { + return nil, err + } + + // BIP0032 hierarchy: m/'/ + // Where purpose = 44 and the ' indicates hardening with the HardenedKeyStart 0x80000000 + purpose, err := root.Child(44 + hdkeychain.HardenedKeyStart) + if err != nil { + return nil, err + } + defer purpose.Zero() + + // BIP0044 hierarchy: m/44'/' + // Where coin type is either the legacy coin type, 20, or the coin type described in SLIP0044, 44. + coinTypePrivKey, err := purpose.Child(coinType + hdkeychain.HardenedKeyStart) + if err != nil { + return nil, err + } + defer coinTypePrivKey.Zero() + + return coinTypePrivKey, nil +} + +// verifySeed checks if a user inputted seed is that of the wallet by comparing their child key derivatied +// public keys. It returns a bool if this the case as well as the current coin type of the wallet. An optional +// parameter is also avalaible that checks beyond default accounts. It returns the result using the RPC api. +func verifySeed(s *Server, icmd interface{}) (interface{}, error) { + cmd := icmd.(*dcrjson.VerifySeedCmd) + w, ok := s.walletLoader.LoadedWallet() + if !ok { + return nil, errUnloadedWallet + } + + coinType, err := w.CoinType() + if err != nil { + return nil, err + } + + decodedSeed, err := walletseed.DecodeUserInput(cmd.Seed) + if err != nil { + return nil, err + } + + // Changed inputted seed, type string, to type byte[] so hdkeychain methods can be utilize using DecodeUserInput + // and run then derivedCoinTypeKey to receive the coin type private key. + coinTypePrivKey, err := deriveCoinTypeKey(decodedSeed, coinType, w.ChainParams()) + if err != nil { + return nil, err + } + defer coinTypePrivKey.Zero() + + // Grab coin type private key from wallet for future comparison to the derived inputted coin type private key + walletCoinTypePrivKey, err := w.CoinTypeKey() + if err != nil { + return nil, err + } + defer walletCoinTypePrivKey.Zero() + + var matches bool + switch { + case cmd.Account != nil: + // Both derivedAccountKey and walletDerivedAccountKey use the BIP044 hierachy: m/44'/'/' + // If the child is a private extended key it is neutered + accountKey, err := coinTypePrivKey.Child(*cmd.Account + hdkeychain.HardenedKeyStart) + if err != nil { + return nil, err + } + defer accountKey.Zero() + + xPubKey, err := accountKey.Neuter() + if err != nil { + return nil, err + } + + walletAccountKey, err := walletCoinTypePrivKey.Child(*cmd.Account + hdkeychain.HardenedKeyStart) + if err != nil { + return nil, err + } + defer walletAccountKey.Zero() + + walletxPubKey, err := walletAccountKey.Neuter() + if err != nil { + return nil, err + } + + // Since the field, key, within the type struct, ExtendedKey, is private - keys must be converted + // to type string for comparison. + matches = xPubKey.String() == walletxPubKey.String() + default: + matches = coinTypePrivKey.String() == walletCoinTypePrivKey.String() + } + + return &dcrjson.VerifySeedResult{ + Result: matches, + CoinType: coinType, + }, nil +} + // version handles the version command by returning the RPC API versions of the // wallet and, optionally, the consensus RPC server as well if it is associated // with the server. The chainClient is optional, and this is simply a helper diff --git a/rpc/legacyrpc/rpcserverhelp.go b/rpc/legacyrpc/rpcserverhelp.go index 5f450daeb..bc66435ab 100644 --- a/rpc/legacyrpc/rpcserverhelp.go +++ b/rpc/legacyrpc/rpcserverhelp.go @@ -56,6 +56,7 @@ func helpDescsEnUS() map[string]string { "sweepaccount": "sweepaccount \"sourceaccount\" \"destinationaddress\" (requiredconfirmations feeperkb)\n\nMoves as much value as possible in a transaction from an account.\n\n\nArguments:\n1. sourceaccount (string, required) The account to be swept.\n2. destinationaddress (string, required) The destination address to pay to.\n3. requiredconfirmations (numeric, optional) The minimum utxo confirmation requirement (optional).\n4. feeperkb (numeric, optional) The minimum relay fee policy (optional).\n\nResult:\n{\n \"unsignedtransaction\": \"value\", (string) The hex encoded string of the unsigned transaction\n \"totalpreviousoutputamount\": n.nnn, (numeric) The total transaction input amount.\n \"totaloutputamount\": n.nnn, (numeric) The total transaction output amount.\n \"estimatedsignedsize\": n, (numeric) The estimated size of the transaction when signed\n} \n", "validateaddress": "validateaddress \"address\"\n\nVerify that an address is valid.\nExtra details are returned if the address is controlled by this wallet.\nThe following fields are valid only when the address is controlled by this wallet (ismine=true): isscript, pubkey, iscompressed, account, addresses, hex, script, and sigsrequired.\nThe following fields are only valid when address has an associated public key: pubkey, iscompressed.\nThe following fields are only valid when address is a pay-to-script-hash address: addresses, hex, and script.\nIf the address is a multisig address controlled by this wallet, the multisig fields will be left unset if the wallet is locked since the redeem script cannot be decrypted.\n\nArguments:\n1. address (string, required) Address to validate\n\nResult:\n{\n \"isvalid\": true|false, (boolean) Whether or not the address is valid\n \"address\": \"value\", (string) The payment address (only when isvalid is true)\n \"ismine\": true|false, (boolean) Whether this address is controlled by the wallet (only when isvalid is true)\n \"iswatchonly\": true|false, (boolean) Unset\n \"isscript\": true|false, (boolean) Whether the payment address is a pay-to-script-hash address (only when isvalid is true)\n \"pubkeyaddr\": \"value\", (string) The pubkey for this payment address (only when isvalid is true)\n \"pubkey\": \"value\", (string) The associated public key of the payment address, if any (only when isvalid is true)\n \"iscompressed\": true|false, (boolean) Whether the address was created by hashing a compressed public key, if any (only when isvalid is true)\n \"account\": \"value\", (string) The account this payment address belongs to (only when isvalid is true)\n \"addresses\": [\"value\",...], (array of string) All associated payment addresses of the script if address is a multisig address (only when isvalid is true)\n \"hex\": \"value\", (string) The redeem script \n \"script\": \"value\", (string) The class of redeem script for a multisig address\n \"sigsrequired\": n, (numeric) The number of required signatures to redeem outputs to the multisig address\n} \n", "verifymessage": "verifymessage \"address\" \"signature\" \"message\"\n\nVerify a message was signed with the associated private key of some address.\n\nArguments:\n1. address (string, required) Address used to sign message\n2. signature (string, required) The signature to verify\n3. message (string, required) The message to verify\n\nResult:\ntrue|false (boolean) Whether the message was signed with the private key of 'address'\n", + "verifyseed": "verifyseed \"seed\" (account)\n\nVerifys if a seed is the same as the running wallet.\n\nArguments:\n1. seed (string, required) Seed to be inputted to check against the wallets master public key, after derivation.\n2. account (numeric, optional) Account number, of potential branch of the master public key, this is an optional input.\n\nResult:\n{\n \"keyresult\": true|false, (boolean) Whether or not if the inputted seed is the same as the running wallets\n \"cointype\": n, (numeric) Outputs the current cointype of the running wallet.\n} \n", "version": "version\n\nReturns application and API versions (semver) keyed by their names\n\nArguments:\nNone\n\nResult:\n{\n \"Program or API name\": Object containing the semantic version, (object) Version objects keyed by the program or API name\n ...\n}\n", "walletlock": "walletlock\n\nLock the wallet.\n\nArguments:\nNone\n\nResult:\nNothing\n", "walletpassphrase": "walletpassphrase \"passphrase\" timeout\n\nUnlock the wallet.\n\nArguments:\n1. passphrase (string, required) The wallet passphrase\n2. timeout (numeric, required) The number of seconds to wait before the wallet automatically locks\n\nResult:\nNothing\n", @@ -86,4 +87,4 @@ var localeHelpDescs = map[string]func() map[string]string{ "en_US": helpDescsEnUS, } -var requestUsages = "accountaddressindex \"account\" branch\naccountsyncaddressindex \"account\" branch index\naddmultisigaddress nrequired [\"key\",...] (\"account\")\nconsolidate inputs (\"account\" \"address\")\ncreatemultisig nrequired [\"key\",...]\ndumpprivkey \"address\"\ngetaccount \"address\"\ngetaccountaddress \"account\"\ngetaddressesbyaccount \"account\"\ngetbalance (\"account\" minconf=1)\ngetbestblockhash\ngetblockcount\ngetinfo\ngetmasterpubkey (\"account\")\ngetmultisigoutinfo \"hash\" index\ngetnewaddress (\"account\" \"gappolicy\")\ngetrawchangeaddress (\"account\")\ngetreceivedbyaccount \"account\" (minconf=1)\ngetreceivedbyaddress \"address\" (minconf=1)\ngettickets includeimmature\ngettransaction \"txid\" (includewatchonly=false)\ngetvotechoices\nhelp (\"command\")\nimportprivkey \"privkey\" (\"label\" rescan=true scanfrom)\nimportscript \"hex\" (rescan=true scanfrom)\nkeypoolrefill (newsize=100)\nlistaccounts (minconf=1)\nlistlockunspent\nlistreceivedbyaccount (minconf=1 includeempty=false includewatchonly=false)\nlistreceivedbyaddress (minconf=1 includeempty=false includewatchonly=false)\nlistsinceblock (\"blockhash\" targetconfirmations=1 includewatchonly=false)\nlisttransactions (\"account\" count=10 from=0 includewatchonly=false)\nlistunspent (minconf=1 maxconf=9999999 [\"address\",...])\nlockunspent unlock [{\"txid\":\"value\",\"vout\":n,\"tree\":n},...]\nredeemmultisigout \"hash\" index tree (\"address\")\nredeemmultisigouts \"fromscraddress\" (\"toaddress\" number)\nrescanwallet (beginheight=0)\nrevoketickets\nsendfrom \"fromaccount\" \"toaddress\" amount (minconf=1 \"comment\" \"commentto\")\nsendmany \"fromaccount\" {\"address\":amount,...} (minconf=1 \"comment\")\nsendtoaddress \"address\" amount (\"comment\" \"commentto\")\nsendtomultisig \"fromaccount\" amount [\"pubkey\",...] (nrequired=1 minconf=1 \"comment\")\nsettxfee amount\nsetvotechoice \"agendaid\" \"choiceid\"\nsignmessage \"address\" \"message\"\nsignrawtransaction \"rawtx\" ([{\"txid\":\"value\",\"vout\":n,\"tree\":n,\"scriptpubkey\":\"value\",\"redeemscript\":\"value\"},...] [\"privkey\",...] flags=\"ALL\")\nsignrawtransactions [\"rawtx\",...] (send=true)\nstartautobuyer \"account\" \"passphrase\" (balancetomaintain maxfeeperkb maxpricerelative maxpriceabsolute \"votingaddress\" \"pooladdress\" poolfees maxperblock)\nstopautobuyer\nsweepaccount \"sourceaccount\" \"destinationaddress\" (requiredconfirmations feeperkb)\nvalidateaddress \"address\"\nverifymessage \"address\" \"signature\" \"message\"\nversion\nwalletlock\nwalletpassphrase \"passphrase\" timeout\nwalletpassphrasechange \"oldpassphrase\" \"newpassphrase\"\ncreatenewaccount \"account\"\nexportwatchingwallet (\"account\" download=false)\ngetbestblock\ngetunconfirmedbalance (\"account\")\nlistaddresstransactions [\"address\",...] (\"account\")\nlistalltransactions (\"account\")\nrenameaccount \"oldaccount\" \"newaccount\"\nwalletislocked\nwalletinfo\npurchaseticket \"fromaccount\" spendlimit (minconf=1 \"ticketaddress\" numtickets \"pooladdress\" poolfees expiry \"comment\" nosplittransaction ticketfee)\ngeneratevote \"blockhash\" height \"tickethash\" votebits \"votebitsext\"\ngetstakeinfo\ngetticketfee\nsetticketfee fee\ngetwalletfee\naddticket \"tickethex\"\nlistscripts\nstakepooluserinfo \"user\"\nticketsforaddress \"address\"" +var requestUsages = "accountaddressindex \"account\" branch\naccountsyncaddressindex \"account\" branch index\naddmultisigaddress nrequired [\"key\",...] (\"account\")\nconsolidate inputs (\"account\" \"address\")\ncreatemultisig nrequired [\"key\",...]\ndumpprivkey \"address\"\ngetaccount \"address\"\ngetaccountaddress \"account\"\ngetaddressesbyaccount \"account\"\ngetbalance (\"account\" minconf=1)\ngetbestblockhash\ngetblockcount\ngetinfo\ngetmasterpubkey (\"account\")\ngetmultisigoutinfo \"hash\" index\ngetnewaddress (\"account\" \"gappolicy\")\ngetrawchangeaddress (\"account\")\ngetreceivedbyaccount \"account\" (minconf=1)\ngetreceivedbyaddress \"address\" (minconf=1)\ngettickets includeimmature\ngettransaction \"txid\" (includewatchonly=false)\ngetvotechoices\nhelp (\"command\")\nimportprivkey \"privkey\" (\"label\" rescan=true scanfrom)\nimportscript \"hex\" (rescan=true scanfrom)\nkeypoolrefill (newsize=100)\nlistaccounts (minconf=1)\nlistlockunspent\nlistreceivedbyaccount (minconf=1 includeempty=false includewatchonly=false)\nlistreceivedbyaddress (minconf=1 includeempty=false includewatchonly=false)\nlistsinceblock (\"blockhash\" targetconfirmations=1 includewatchonly=false)\nlisttransactions (\"account\" count=10 from=0 includewatchonly=false)\nlistunspent (minconf=1 maxconf=9999999 [\"address\",...])\nlockunspent unlock [{\"txid\":\"value\",\"vout\":n,\"tree\":n},...]\nredeemmultisigout \"hash\" index tree (\"address\")\nredeemmultisigouts \"fromscraddress\" (\"toaddress\" number)\nrescanwallet (beginheight=0)\nrevoketickets\nsendfrom \"fromaccount\" \"toaddress\" amount (minconf=1 \"comment\" \"commentto\")\nsendmany \"fromaccount\" {\"address\":amount,...} (minconf=1 \"comment\")\nsendtoaddress \"address\" amount (\"comment\" \"commentto\")\nsendtomultisig \"fromaccount\" amount [\"pubkey\",...] (nrequired=1 minconf=1 \"comment\")\nsettxfee amount\nsetvotechoice \"agendaid\" \"choiceid\"\nsignmessage \"address\" \"message\"\nsignrawtransaction \"rawtx\" ([{\"txid\":\"value\",\"vout\":n,\"tree\":n,\"scriptpubkey\":\"value\",\"redeemscript\":\"value\"},...] [\"privkey\",...] flags=\"ALL\")\nsignrawtransactions [\"rawtx\",...] (send=true)\nstartautobuyer \"account\" \"passphrase\" (balancetomaintain maxfeeperkb maxpricerelative maxpriceabsolute \"votingaddress\" \"pooladdress\" poolfees maxperblock)\nstopautobuyer\nsweepaccount \"sourceaccount\" \"destinationaddress\" (requiredconfirmations feeperkb)\nvalidateaddress \"address\"\nverifymessage \"address\" \"signature\" \"message\"\nverifyseed \"seed\" (account)\nversion\nwalletlock\nwalletpassphrase \"passphrase\" timeout\nwalletpassphrasechange \"oldpassphrase\" \"newpassphrase\"\ncreatenewaccount \"account\"\nexportwatchingwallet (\"account\" download=false)\ngetbestblock\ngetunconfirmedbalance (\"account\")\nlistaddresstransactions [\"address\",...] (\"account\")\nlistalltransactions (\"account\")\nrenameaccount \"oldaccount\" \"newaccount\"\nwalletislocked\nwalletinfo\npurchaseticket \"fromaccount\" spendlimit (minconf=1 \"ticketaddress\" numtickets \"pooladdress\" poolfees expiry \"comment\" nosplittransaction ticketfee)\ngeneratevote \"blockhash\" height \"tickethash\" votebits \"votebitsext\"\ngetstakeinfo\ngetticketfee\nsetticketfee fee\ngetwalletfee\naddticket \"tickethex\"\nlistscripts\nstakepooluserinfo \"user\"\nticketsforaddress \"address\""