Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Key import and export cli commands #7546

Merged
merged 7 commits into from
Aug 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions core/commands/commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,8 @@ func TestCommands(t *testing.T) {
"/id",
"/key",
"/key/gen",
"/key/export",
"/key/import",
"/key/list",
"/key/rename",
"/key/rm",
Expand Down
168 changes: 168 additions & 0 deletions core/commands/keystore.go
Original file line number Diff line number Diff line change
@@ -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"
)
Expand All @@ -31,6 +39,8 @@ publish'.
},
Subcommands: map[string]*cmds.Command{
"gen": keyGenCmd,
"export": keyExportCmd,
"import": keyImportCmd,
"list": keyListCmd,
"rename": keyRenameCmd,
"rm": keyRmCmd,
Expand Down Expand Up @@ -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-name>.key', but an alternate
path can be specified with '--output=<path>' or '-o=<path>'.
`,
},
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'")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm. Is there any reason not to allow this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess not, but we have to the keys live in different places and it's a little messier. Let's add it in a followup PR

}

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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rendaw I switched us from using base58btc encoded strings to just sending the raw bytes over the wire and working with files.

While in the future it's pretty easy for us to add new base encodings to import/export (e.g. --type=raw/multibase) it's actually pretty painful for us to allow for imports to be strings OR files.

I went with files over strings here as it seems likely to me that people will want to ultimately store these blobs in files. Additionally, the keystore currently utilizes files so people are likely already used to backing up keys this way.

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",
Expand Down
102 changes: 88 additions & 14 deletions test/sharness/t0165-keystore.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 &&
Expand All @@ -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 &&
Expand All @@ -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 "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 "both keys show up in list output" '
echo bazed > list_exp &&
echo foobarsa >> list_exp &&
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" '
Expand All @@ -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" '
Expand All @@ -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" '
Expand All @@ -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