diff --git a/cmd/kaspawallet/config.go b/cmd/kaspawallet/config.go index 4f5879def..6bb7a0320 100644 --- a/cmd/kaspawallet/config.go +++ b/cmd/kaspawallet/config.go @@ -12,10 +12,11 @@ const ( createSubCmd = "create" balanceSubCmd = "balance" sendSubCmd = "send" - createUnsignedTransactionSubCmd = "createUnsignedTransaction" + createUnsignedTransactionSubCmd = "create-unsigned-transaction" signSubCmd = "sign" broadcastSubCmd = "broadcast" showAddressSubCmd = "show-address" + dumpUnencryptedDataSubCmd = "dump-unencrypted-data" ) type configFlags struct { @@ -70,6 +71,11 @@ type showAddressConfig struct { config.NetworkFlags } +type dumpUnencryptedDataConfig struct { + KeysFile string `long:"keys-file" short:"f" description:"Keys file location (default: ~/.kaspawallet/keys.json (*nix), %USERPROFILE%\\AppData\\Local\\Kaspawallet\\key.json (Windows))"` + config.NetworkFlags +} + func parseCommandLine() (subCommand string, config interface{}) { cfg := &configFlags{} parser := flags.NewParser(cfg, flags.PrintErrors|flags.HelpFlag) @@ -102,6 +108,11 @@ func parseCommandLine() (subCommand string, config interface{}) { parser.AddCommand(showAddressSubCmd, "Shows the public address of the current wallet", "Shows the public address of the current wallet", showAddressConf) + dumpUnencryptedDataConf := &dumpUnencryptedDataConfig{} + parser.AddCommand(dumpUnencryptedDataSubCmd, "Prints the unencrypted wallet data", + "Prints the unencrypted wallet data including its private keys. Anyone that sees it can access "+ + "the funds. Use only on safe environment.", dumpUnencryptedDataConf) + _, err := parser.Parse() if err != nil { @@ -164,6 +175,13 @@ func parseCommandLine() (subCommand string, config interface{}) { printErrorAndExit(err) } config = showAddressConf + case dumpUnencryptedDataSubCmd: + combineNetworkFlags(&dumpUnencryptedDataConf.NetworkFlags, &cfg.NetworkFlags) + err := dumpUnencryptedDataConf.ResolveNetwork(parser) + if err != nil { + printErrorAndExit(err) + } + config = dumpUnencryptedDataConf } return parser.Command.Active.Name, config diff --git a/cmd/kaspawallet/dump_unencrypted_data.go b/cmd/kaspawallet/dump_unencrypted_data.go new file mode 100644 index 000000000..bb680de74 --- /dev/null +++ b/cmd/kaspawallet/dump_unencrypted_data.go @@ -0,0 +1,69 @@ +package main + +import ( + "bufio" + "fmt" + "github.com/kaspanet/kaspad/cmd/kaspawallet/keys" + "github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet" + "github.com/pkg/errors" + "os" +) + +func dumpUnencryptedData(conf *dumpUnencryptedDataConfig) error { + err := confirmDump() + if err != nil { + return err + } + + keysFile, err := keys.ReadKeysFile(conf.KeysFile) + if err != nil { + return err + } + + privateKeys, err := keysFile.DecryptPrivateKeys() + if err != nil { + return err + } + + privateKeysPublicKeys := make(map[string]struct{}) + for i, privateKey := range privateKeys { + fmt.Printf("Private key #%d:\n%x\n\n", i+1, privateKey) + publicKey, err := libkaspawallet.PublicKeyFromPrivateKey(privateKey) + if err != nil { + return err + } + + privateKeysPublicKeys[string(publicKey)] = struct{}{} + } + + i := 1 + for _, publicKey := range keysFile.PublicKeys { + if _, exists := privateKeysPublicKeys[string(publicKey)]; exists { + continue + } + + fmt.Printf("Public key #%d:\n%x\n\n", i, publicKey) + i++ + } + + fmt.Printf("Minimum number of signatures: %d\n", keysFile.MinimumSignatures) + return nil +} + +func confirmDump() error { + reader := bufio.NewReader(os.Stdin) + fmt.Printf("This operation will print your unencrypted keys on the screen. Anyone that sees this information " + + "will be able to steal your funds. Are you sure you want to proceed (y/N)? ") + line, isPrefix, err := reader.ReadLine() + if err != nil { + return err + } + + fmt.Println() + + if isPrefix || string(line) != "y" { + return errors.Errorf("Dump aborted by user") + } + + return nil +} diff --git a/cmd/kaspawallet/keys/create.go b/cmd/kaspawallet/keys/create.go index c6a48bad4..3e12ec568 100644 --- a/cmd/kaspawallet/keys/create.go +++ b/cmd/kaspawallet/keys/create.go @@ -4,6 +4,7 @@ import ( "bufio" "crypto/rand" "crypto/subtle" + "encoding/hex" "fmt" "github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet" "github.com/pkg/errors" @@ -29,7 +30,17 @@ func ImportKeyPairs(numKeys uint32) (encryptedPrivateKeys []*EncryptedPrivateKey if isPrefix { return nil, nil, errors.Errorf("Private key is too long") } - return libkaspawallet.KeyPairFromPrivateKeyHex(string(line)) + privateKey, err := hex.DecodeString(string(line)) + if err != nil { + return nil, nil, err + } + + publicKey, err := libkaspawallet.PublicKeyFromPrivateKey(privateKey) + if err != nil { + return nil, nil, err + } + + return privateKey, publicKey, nil }) } diff --git a/cmd/kaspawallet/libkaspawallet/keypair.go b/cmd/kaspawallet/libkaspawallet/keypair.go index 1043fdb40..7385d803f 100644 --- a/cmd/kaspawallet/libkaspawallet/keypair.go +++ b/cmd/kaspawallet/libkaspawallet/keypair.go @@ -1,7 +1,6 @@ package libkaspawallet import ( - "encoding/hex" "github.com/kaspanet/go-secp256k1" "github.com/kaspanet/kaspad/domain/dagconfig" "github.com/kaspanet/kaspad/util" @@ -10,24 +9,40 @@ import ( // CreateKeyPair generates a private-public key pair func CreateKeyPair() ([]byte, []byte, error) { - privateKey, err := secp256k1.GenerateSchnorrKeyPair() + keyPair, err := secp256k1.GenerateSchnorrKeyPair() if err != nil { return nil, nil, errors.Wrap(err, "Failed to generate private key") } - return keyPairBytes(privateKey) + publicKey, err := keyPair.SchnorrPublicKey() + if err != nil { + return nil, nil, errors.Wrap(err, "Failed to generate public key") + } + publicKeySerialized, err := publicKey.Serialize() + if err != nil { + return nil, nil, errors.Wrap(err, "Failed to serialize public key") + } + + return keyPair.SerializePrivateKey()[:], publicKeySerialized[:], nil } -// KeyPairFromPrivateKeyHex decodes a private-public key pair out of `privateKeyHex` -func KeyPairFromPrivateKeyHex(privateKeyHex string) ([]byte, []byte, error) { - privateKeyBytes, err := hex.DecodeString(privateKeyHex) +// PublicKeyFromPrivateKey returns the public key associated with a private key +func PublicKeyFromPrivateKey(privateKeyBytes []byte) ([]byte, error) { + keyPair, err := secp256k1.DeserializeSchnorrPrivateKeyFromSlice(privateKeyBytes) if err != nil { - return nil, nil, errors.Wrap(err, "Failed to deserialized private key") + return nil, errors.Wrap(err, "Failed to deserialized private key") } - privateKey, err := secp256k1.DeserializeSchnorrPrivateKeyFromSlice(privateKeyBytes) + + publicKey, err := keyPair.SchnorrPublicKey() if err != nil { - return nil, nil, errors.Wrap(err, "Failed to deserialized private key") + return nil, errors.Wrap(err, "Failed to generate public key") } - return keyPairBytes(privateKey) + + publicKeySerialized, err := publicKey.Serialize() + if err != nil { + return nil, errors.Wrap(err, "Failed to serialize public key") + } + + return publicKeySerialized[:], nil } func keyPairBytes(keyPair *secp256k1.SchnorrKeyPair) ([]byte, []byte, error) { diff --git a/cmd/kaspawallet/main.go b/cmd/kaspawallet/main.go index 1acecc59e..959e1f497 100644 --- a/cmd/kaspawallet/main.go +++ b/cmd/kaspawallet/main.go @@ -21,6 +21,8 @@ func main() { err = broadcast(config.(*broadcastConfig)) case showAddressSubCmd: err = showAddress(config.(*showAddressConfig)) + case dumpUnencryptedDataSubCmd: + err = dumpUnencryptedData(config.(*dumpUnencryptedDataConfig)) default: err = errors.Errorf("Unknown sub-command '%s'\n", subCmd) }