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

basic keystore implementation #3472

Merged
merged 3 commits into from
Dec 6, 2016
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
158 changes: 158 additions & 0 deletions core/commands/keystore.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package commands

import (
"crypto/rand"
"fmt"
"io"
"sort"
"strings"

cmds "github.com/ipfs/go-ipfs/commands"

peer "gx/ipfs/QmfMmLGoKzCHDN7cGgk64PJr4iipzidDRME8HABSJqvmhC/go-libp2p-peer"
ci "gx/ipfs/QmfWDLQjGjVe4fr5CoztYW2DYYjRysMJrFe1RCsXLPTf46/go-libp2p-crypto"
)

var KeyCmd = &cmds.Command{
Helptext: cmds.HelpText{
Tagline: "Create and manipulate keypairs",
},
Subcommands: map[string]*cmds.Command{
"gen": KeyGenCmd,
"list": KeyListCmd,
},
}

type KeyOutput struct {
Name string
Id string
}

var KeyGenCmd = &cmds.Command{
Helptext: cmds.HelpText{
Tagline: "Create a new keypair",
},
Options: []cmds.Option{
cmds.StringOption("type", "t", "type of the key to create"),
cmds.IntOption("size", "s", "size of the key to generate"),
},
Arguments: []cmds.Argument{
cmds.StringArg("name", true, false, "name of key to create"),
},
Run: func(req cmds.Request, res cmds.Response) {
n, err := req.InvocContext().GetNode()
if err != nil {
res.SetError(err, cmds.ErrNormal)
return
}

typ, f, err := req.Option("type").String()
if err != nil {
res.SetError(err, cmds.ErrNormal)
return
}

if !f {
res.SetError(fmt.Errorf("please specify a key type with --type"), cmds.ErrNormal)
return
}

size, sizefound, err := req.Option("size").Int()
if err != nil {
res.SetError(err, cmds.ErrNormal)
return
}

name := req.Arguments()[0]
if name == "self" {
res.SetError(fmt.Errorf("cannot create key with name 'self'"), cmds.ErrNormal)
return
}

var sk ci.PrivKey
var pk ci.PubKey

switch typ {
case "rsa":
if !sizefound {
res.SetError(fmt.Errorf("please specify a key size with --size"), cmds.ErrNormal)
return
}

priv, pub, err := ci.GenerateKeyPairWithReader(ci.RSA, size, rand.Reader)
if err != nil {
res.SetError(err, cmds.ErrNormal)
return
}

sk = priv
pk = pub
case "ed25519":
priv, pub, err := ci.GenerateEd25519Key(rand.Reader)
if err != nil {
res.SetError(err, cmds.ErrNormal)
return
}

sk = priv
pk = pub
default:
res.SetError(fmt.Errorf("unrecognized key type: %s", typ), cmds.ErrNormal)
return
}

err = n.Repo.Keystore().Put(name, sk)
if err != nil {
res.SetError(err, cmds.ErrNormal)
return
}

pid, err := peer.IDFromPublicKey(pk)
if err != nil {
res.SetError(err, cmds.ErrNormal)
return
}

res.SetOutput(&KeyOutput{
Name: name,
Id: pid.Pretty(),
})
},
Marshalers: cmds.MarshalerMap{
cmds.Text: func(res cmds.Response) (io.Reader, error) {
k, ok := res.Output().(*KeyOutput)
if !ok {
return nil, fmt.Errorf("expected a KeyOutput as command result")
}

return strings.NewReader(k.Id), nil
},
},
Type: KeyOutput{},
}

var KeyListCmd = &cmds.Command{
Helptext: cmds.HelpText{
Tagline: "List all local keypairs",
},
Run: func(req cmds.Request, res cmds.Response) {
n, err := req.InvocContext().GetNode()
if err != nil {
res.SetError(err, cmds.ErrNormal)
return
}

keys, err := n.Repo.Keystore().List()
if err != nil {
res.SetError(err, cmds.ErrNormal)
return
}

sort.Strings(keys)
res.SetOutput(&stringList{keys})
},
Marshalers: cmds.MarshalerMap{
cmds.Text: stringListMarshaler,
},
Type: stringList{},
}
16 changes: 15 additions & 1 deletion core/commands/publish.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ Publish an <ipfs-path> to another public key (not implemented):
This accepts durations such as "300s", "1.5h" or "2h45m". Valid time units are
"ns", "us" (or "µs"), "ms", "s", "m", "h".`).Default("24h"),
cmds.StringOption("ttl", "Time duration this record should be cached for (caution: experimental)."),
cmds.StringOption("key", "k", "name of key to use").Default("self"),
},
Run: func(req cmds.Request, res cmds.Response) {
log.Debug("begin publish")
Expand Down Expand Up @@ -109,7 +110,20 @@ Publish an <ipfs-path> to another public key (not implemented):
ctx = context.WithValue(ctx, "ipns-publish-ttl", d)
}

output, err := publish(ctx, n, n.PrivateKey, path.Path(pstr), popts)
var k crypto.PrivKey
kname, _, _ := req.Option("key").String()
if kname == "self" {
k = n.PrivateKey
} else {
ksk, err := n.Repo.Keystore().Get(kname)
if err != nil {
res.SetError(err, cmds.ErrNormal)
return
}
k = ksk
}

output, err := publish(ctx, n, k, path.Path(pstr), popts)
if err != nil {
res.SetError(err, cmds.ErrNormal)
return
Expand Down
1 change: 1 addition & 0 deletions core/commands/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ var rootSubcommands = map[string]*cmds.Command{
"files": files.FilesCmd,
"get": GetCmd,
"id": IDCmd,
"key": KeyCmd,
"log": LogCmd,
"ls": LsCmd,
"mount": MountCmd,
Expand Down
123 changes: 123 additions & 0 deletions keystore/keystore.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package keystore

import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"

ci "gx/ipfs/QmfWDLQjGjVe4fr5CoztYW2DYYjRysMJrFe1RCsXLPTf46/go-libp2p-crypto"
)

type Keystore interface {
Put(string, ci.PrivKey) error
Get(string) (ci.PrivKey, error)
Delete(string) error
List() ([]string, error)
}

var ErrNoSuchKey = fmt.Errorf("no key by the given name was found")
var ErrKeyExists = fmt.Errorf("key by that name already exists, refusing to overwrite")

type FSKeystore struct {
dir string
}

func validateName(name string) error {
if name == "" {
return fmt.Errorf("key names must be at least one character")
}

if strings.Contains(name, "/") {
return fmt.Errorf("key names may not contain slashes")
}

if strings.HasPrefix(name, ".") {
return fmt.Errorf("key names may not begin with a period")
}

return nil
}

func NewFSKeystore(dir string) (*FSKeystore, error) {
_, err := os.Stat(dir)
if err != nil {
if !os.IsNotExist(err) {
return nil, err
}
if err := os.Mkdir(dir, 0700); err != nil {
return nil, err
}
}

return &FSKeystore{dir}, nil
}

func (ks *FSKeystore) Put(name string, k ci.PrivKey) error {
if err := validateName(name); err != nil {
return err
}

b, err := k.Bytes()
if err != nil {
return err
}

kp := filepath.Join(ks.dir, name)

_, err = os.Stat(kp)
if err == nil {
Copy link
Member

Choose a reason for hiding this comment

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

To be safe I would use os.IsExist and change it to ErrKeyExists then, else passthrough the error.

Copy link
Member Author

Choose a reason for hiding this comment

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

How would you use os.IsExist here?

Copy link
Member

Choose a reason for hiding this comment

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

sorry, IsNotExist

Copy link
Member Author

Choose a reason for hiding this comment

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

ah! okay, i see what you want

return ErrKeyExists
}

fi, err := os.Create(kp)
if err != nil {
return err
}
defer fi.Close()

_, err = fi.Write(b)
Copy link
Member

Choose a reason for hiding this comment

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

I thought you wanted to use multicodec so we can migrate much easier?

Copy link
Member Author

Choose a reason for hiding this comment

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

Nah, it won't make migrating easier at all. (it will make them hard since i'll have to decode multicodecs in the migrations code :P )

if err != nil {
return err
}

return nil
}

func (ks *FSKeystore) Get(name string) (ci.PrivKey, error) {
if err := validateName(name); err != nil {
return nil, err
}

kp := filepath.Join(ks.dir, name)

data, err := ioutil.ReadFile(kp)
if err != nil {
if os.IsNotExist(err) {
return nil, ErrNoSuchKey
}
return nil, err
}

return ci.UnmarshalPrivateKey(data)
}

func (ks *FSKeystore) Delete(name string) error {
if err := validateName(name); err != nil {
return err
}

kp := filepath.Join(ks.dir, name)

return os.Remove(kp)
}

func (ks *FSKeystore) List() ([]string, error) {
dir, err := os.Open(ks.dir)
if err != nil {
return nil, err
}

return dir.Readdirnames(0)
}
55 changes: 55 additions & 0 deletions keystore/memkeystore.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package keystore

import ci "gx/ipfs/QmfWDLQjGjVe4fr5CoztYW2DYYjRysMJrFe1RCsXLPTf46/go-libp2p-crypto"

type MemKeystore struct {
keys map[string]ci.PrivKey
}

func NewMemKeystore() *MemKeystore {
return &MemKeystore{make(map[string]ci.PrivKey)}
}

func (mk *MemKeystore) Put(name string, k ci.PrivKey) error {
if err := validateName(name); err != nil {
return err
}

_, ok := mk.keys[name]
if ok {
return ErrKeyExists
}

mk.keys[name] = k
return nil
}

func (mk *MemKeystore) Get(name string) (ci.PrivKey, error) {
if err := validateName(name); err != nil {
return nil, err
}

k, ok := mk.keys[name]
if !ok {
return nil, ErrNoSuchKey
}

return k, nil
}

func (mk *MemKeystore) Delete(name string) error {
if err := validateName(name); err != nil {
return err
}

delete(mk.keys, name)
return nil
}

func (mk *MemKeystore) List() ([]string, error) {
out := make([]string, 0, len(mk.keys))
for k, _ := range mk.keys {
out = append(out, k)
}
return out, nil
}
Loading