diff --git a/core/commands/commands_test.go b/core/commands/commands_test.go index 940b9e0f4..96d7c5010 100644 --- a/core/commands/commands_test.go +++ b/core/commands/commands_test.go @@ -133,6 +133,8 @@ func TestCommands(t *testing.T) { "/id", "/key", "/key/gen", + "/key/export", + "/key/import", "/key/list", "/key/rename", "/key/rm", diff --git a/core/commands/keystore.go b/core/commands/keystore.go index c9865fd93..9089caf60 100644 --- a/core/commands/keystore.go +++ b/core/commands/keystore.go @@ -1,13 +1,21 @@ package commands import ( + "bytes" "fmt" "io" + "io/ioutil" + "os" + "path/filepath" + "strings" "text/tabwriter" cmds "github.com/ipfs/go-ipfs-cmds" cmdenv "github.com/ipfs/go-ipfs/core/commands/cmdenv" + "github.com/ipfs/go-ipfs/core/commands/e" + fsrepo "github.com/ipfs/go-ipfs/repo/fsrepo" options "github.com/ipfs/interface-go-ipfs-core/options" + "github.com/libp2p/go-libp2p-core/crypto" peer "github.com/libp2p/go-libp2p-core/peer" mbase "github.com/multiformats/go-multibase" ) @@ -31,6 +39,8 @@ publish'. }, Subcommands: map[string]*cmds.Command{ "gen": keyGenCmd, + "export": keyExportCmd, + "import": keyImportCmd, "list": keyListCmd, "rename": keyRenameCmd, "rm": keyRmCmd, @@ -143,6 +153,164 @@ func formatID(id peer.ID, formatLabel string) string { } } +var keyExportCmd = &cmds.Command{ + Helptext: cmds.HelpText{ + Tagline: "Export a keypair", + ShortDescription: ` +Exports a named libp2p key to disk. + +By default, the output will be stored at './.key', but an alternate +path can be specified with '--output=' or '-o='. +`, + }, + Arguments: []cmds.Argument{ + cmds.StringArg("name", true, false, "name of key to export").EnableStdin(), + }, + Options: []cmds.Option{ + cmds.StringOption(outputOptionName, "o", "The path where the output should be stored."), + }, + Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error { + name := req.Arguments[0] + + if name == "self" { + return fmt.Errorf("cannot export key with name 'self'") + } + + cfgRoot, err := cmdenv.GetConfigRoot(env) + if err != nil { + return err + } + + r, err := fsrepo.Open(cfgRoot) + if err != nil { + return err + } + defer r.Close() + + sk, err := r.Keystore().Get(name) + if err != nil { + return fmt.Errorf("key with name '%s' doesn't exist", name) + } + + encoded, err := crypto.MarshalPrivateKey(sk) + if err != nil { + return err + } + + return res.Emit(bytes.NewReader(encoded)) + }, + PostRun: cmds.PostRunMap{ + cmds.CLI: func(res cmds.Response, re cmds.ResponseEmitter) error { + req := res.Request() + + v, err := res.Next() + if err != nil { + return err + } + + outReader, ok := v.(io.Reader) + if !ok { + return e.New(e.TypeErr(outReader, v)) + } + + outPath, _ := req.Options[outputOptionName].(string) + if outPath == "" { + trimmed := strings.TrimRight(fmt.Sprintf("%s.key", req.Arguments[0]), "/") + _, outPath = filepath.Split(trimmed) + outPath = filepath.Clean(outPath) + } + + // create file + file, err := os.Create(outPath) + if err != nil { + return err + } + defer file.Close() + + _, err = io.Copy(file, outReader) + if err != nil { + return err + } + + return nil + }, + }, +} + +var keyImportCmd = &cmds.Command{ + Helptext: cmds.HelpText{ + Tagline: "Import a key and prints imported key id", + }, + Options: []cmds.Option{ + cmds.StringOption(keyFormatOptionName, "f", "output format: b58mh or b36cid").WithDefault("b58mh"), + }, + Arguments: []cmds.Argument{ + cmds.StringArg("name", true, false, "name to associate with key in keychain"), + cmds.FileArg("key", true, false, "key provided by generate or export"), + }, + Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error { + name := req.Arguments[0] + + if name == "self" { + return fmt.Errorf("cannot import key with name 'self'") + } + + file, err := cmdenv.GetFileArg(req.Files.Entries()) + if err != nil { + return err + } + defer file.Close() + + data, err := ioutil.ReadAll(file) + if err != nil { + return err + } + + sk, err := crypto.UnmarshalPrivateKey(data) + if err != nil { + return err + } + + cfgRoot, err := cmdenv.GetConfigRoot(env) + if err != nil { + return err + } + + r, err := fsrepo.Open(cfgRoot) + if err != nil { + return err + } + defer r.Close() + + _, err = r.Keystore().Get(name) + if err == nil { + return fmt.Errorf("key with name '%s' already exists", name) + } + + err = r.Keystore().Put(name, sk) + if err != nil { + return err + } + + pid, err := peer.IDFromPrivateKey(sk) + if err != nil { + return err + } + + return cmds.EmitOnce(res, &KeyOutput{ + Name: name, + Id: formatID(pid, req.Options[keyFormatOptionName].(string)), + }) + }, + Encoders: cmds.EncoderMap{ + cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, ko *KeyOutput) error { + _, err := w.Write([]byte(ko.Id + "\n")) + return err + }), + }, + Type: KeyOutput{}, +} + var keyListCmd = &cmds.Command{ Helptext: cmds.HelpText{ Tagline: "List all local keypairs", diff --git a/test/sharness/t0165-keystore.sh b/test/sharness/t0165-keystore.sh index c4538c70e..f75c0d5d3 100755 --- a/test/sharness/t0165-keystore.sh +++ b/test/sharness/t0165-keystore.sh @@ -17,6 +17,12 @@ PEERID=$(ipfs key gen -f=b58mh --type=rsa --size=2048 key_rsa) && test_check_rsa2048_b58mh_peerid $PEERID ' +test_expect_success "test RSA key sk export format" ' +ipfs key export key_rsa && +test_check_rsa2048_sk key_rsa.key && +rm key_rsa.key +' + test_expect_success "test RSA key B36CID multihash format" ' PEERID=$(ipfs key list -f=b36cid -l | grep key_rsa | head -n 1 | cut -d " " -f1) && test_check_rsa2048_b36cid_peerid $PEERID && @@ -28,6 +34,12 @@ PEERID=$(ipfs key gen -f=b36cid --type=ed25519 key_ed25519) && test_check_ed25519_b36cid_peerid $PEERID ' +test_expect_success "test ED25519 key sk export format" ' +ipfs key export key_ed25519 && +test_check_ed25519_sk key_ed25519.key && +rm key_ed25519.key +' + test_expect_success "test ED25519 key B36CID multihash format" ' PEERID=$(ipfs key list -f=b36cid -l | grep key_ed25519 | head -n 1 | cut -d " " -f1) && test_check_ed25519_b36cid_peerid $PEERID && @@ -37,19 +49,63 @@ ipfs key rm key_ed25519 test_expect_success "create a new rsa key" ' - rsahash=$(ipfs key gen -f=b58mh foobarsa --type=rsa --size=2048) + rsahash=$(ipfs key gen -f=b58mh generated_rsa_key --type=rsa --size=2048) + echo $rsahash > rsa_key_id ' test_expect_success "create a new ed25519 key" ' - edhash=$(ipfs key gen -f=b58mh bazed --type=ed25519) + edhash=$(ipfs key gen -f=b58mh generated_ed25519_key --type=ed25519) + echo $edhash > ed25519_key_id ' - test_expect_success "both keys show up in list output" ' - echo bazed > list_exp && - echo foobarsa >> list_exp && + test_expect_success "export and import rsa key" ' + ipfs key export generated_rsa_key && + ipfs key rm generated_rsa_key && + ipfs key import generated_rsa_key generated_rsa_key.key > roundtrip_rsa_key_id && + test_cmp rsa_key_id roundtrip_rsa_key_id + ' + + test_expect_success "export and import ed25519 key" ' + ipfs key export generated_ed25519_key && + ipfs key rm generated_ed25519_key && + ipfs key import generated_ed25519_key generated_ed25519_key.key > roundtrip_ed25519_key_id && + test_cmp ed25519_key_id roundtrip_ed25519_key_id + ' + + test_expect_success "test export file option" ' + ipfs key export generated_rsa_key -o=named_rsa_export_file && + test_cmp generated_rsa_key.key named_rsa_export_file && + ipfs key export generated_ed25519_key -o=named_ed25519_export_file && + test_cmp generated_ed25519_key.key named_ed25519_export_file + ' + + test_expect_success "key export can't export self" ' + test_must_fail ipfs key export self 2>&1 | tee key_exp_out && + grep -q "Error: cannot export key with name" key_exp_out && + test_must_fail ipfs key export self -o=selfexport 2>&1 | tee key_exp_out && + grep -q "Error: cannot export key with name" key_exp_out + ' + + test_expect_success "key import can't import self" ' + ipfs key gen overwrite_self_import && + ipfs key export overwrite_self_import && + test_must_fail ipfs key import self overwrite_self_import.key 2>&1 | tee key_imp_out && + grep -q "Error: cannot import key with name" key_imp_out && + ipfs key rm overwrite_self_import && + rm overwrite_self_import.key + ' + + test_expect_success "add a default key" ' + ipfs key gen quxel + ' + + test_expect_success "all keys show up in list output" ' + echo generated_ed25519_key > list_exp && + echo generated_rsa_key >> list_exp && + echo quxel >> list_exp && echo self >> list_exp - ipfs key list -f=b58mh | sort > list_out && - test_cmp list_exp list_out + ipfs key list -f=b58mh > list_out && + test_sort_cmp list_exp list_out ' test_expect_success "key hashes show up in long list output" ' @@ -63,11 +119,12 @@ ipfs key rm key_ed25519 ' test_expect_success "key rm remove a key" ' - ipfs key rm foobarsa - echo bazed > list_exp && + ipfs key rm generated_rsa_key + echo generated_ed25519_key > list_exp && + echo quxel >> list_exp && echo self >> list_exp - ipfs key list -f=b58mh | sort > list_out && - test_cmp list_exp list_out + ipfs key list -f=b58mh > list_out && + test_sort_cmp list_exp list_out ' test_expect_success "key rm can't remove self" ' @@ -76,11 +133,12 @@ ipfs key rm key_ed25519 ' test_expect_success "key rename rename a key" ' - ipfs key rename bazed fooed + ipfs key rename generated_ed25519_key fooed echo fooed > list_exp && + echo quxel >> list_exp && echo self >> list_exp - ipfs key list -f=b58mh | sort > list_out && - test_cmp list_exp list_out + ipfs key list -f=b58mh > list_out && + test_sort_cmp list_exp list_out ' test_expect_success "key rename rename key output succeeds" ' @@ -101,6 +159,22 @@ ipfs key rm key_ed25519 ' } +test_check_rsa2048_sk() { + sklen=$(ls -l $1 | awk '{print $5}') && + test "$sklen" -lt "1600" && test "$sklen" -gt "1000" || { + echo "Bad RSA2048 sk '$1' with len '$sklen'" + return 1 + } +} + +test_check_ed25519_sk() { + sklen=$(ls -l $1 | awk '{print $5}') && + test "$sklen" -lt "100" && test "$sklen" -gt "30" || { + echo "Bad ED25519 sk '$1' with len '$sklen'" + return 1 + } +} + test_key_cmd test_done