From fee2b7fcf2e9b99afa3d888bfda23a5e2969a613 Mon Sep 17 00:00:00 2001 From: Jeromy Date: Wed, 30 Nov 2016 10:18:09 -0800 Subject: [PATCH] basic keystore implementation License: MIT Signed-off-by: Jeromy --- core/commands/keystore.go | 158 ++++++++++++++++++++++++++++++++++++++ core/commands/publish.go | 20 ++++- core/commands/root.go | 1 + keystore/keystore.go | 123 +++++++++++++++++++++++++++++ keystore/memkeystore.go | 55 +++++++++++++ repo/fsrepo/fsrepo.go | 22 ++++++ repo/mock.go | 4 + repo/repo.go | 3 + 8 files changed, 383 insertions(+), 3 deletions(-) create mode 100644 core/commands/keystore.go create mode 100644 keystore/keystore.go create mode 100644 keystore/memkeystore.go diff --git a/core/commands/keystore.go b/core/commands/keystore.go new file mode 100644 index 000000000000..efa33d5d7c89 --- /dev/null +++ b/core/commands/keystore.go @@ -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{}, +} diff --git a/core/commands/publish.go b/core/commands/publish.go index 357868492313..bed21a7a497e 100644 --- a/core/commands/publish.go +++ b/core/commands/publish.go @@ -1,17 +1,17 @@ package commands import ( + "context" "errors" "fmt" "io" "strings" "time" - context "context" - cmds "github.com/ipfs/go-ipfs/commands" core "github.com/ipfs/go-ipfs/core" path "github.com/ipfs/go-ipfs/path" + key "gx/ipfs/QmYEoKZXHoAToWfhGF3vryhMn3WWhE1o2MasQ8uzY5iDi9/go-key" crypto "gx/ipfs/QmfWDLQjGjVe4fr5CoztYW2DYYjRysMJrFe1RCsXLPTf46/go-libp2p-crypto" ) @@ -56,6 +56,7 @@ Publish an 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") @@ -109,7 +110,20 @@ Publish an 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 diff --git a/core/commands/root.go b/core/commands/root.go index d2d6cf550f55..7070a2d27ec6 100644 --- a/core/commands/root.go +++ b/core/commands/root.go @@ -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, diff --git a/keystore/keystore.go b/keystore/keystore.go new file mode 100644 index 000000000000..19e77f17333b --- /dev/null +++ b/keystore/keystore.go @@ -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 { + return ErrKeyExists + } + + fi, err := os.Create(kp) + if err != nil { + return err + } + defer fi.Close() + + _, err = fi.Write(b) + 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) +} diff --git a/keystore/memkeystore.go b/keystore/memkeystore.go new file mode 100644 index 000000000000..9eec377db960 --- /dev/null +++ b/keystore/memkeystore.go @@ -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 +} diff --git a/repo/fsrepo/fsrepo.go b/repo/fsrepo/fsrepo.go index 05582a8ea84c..3179a479db10 100644 --- a/repo/fsrepo/fsrepo.go +++ b/repo/fsrepo/fsrepo.go @@ -11,6 +11,7 @@ import ( "sync" "github.com/ipfs/go-ipfs/Godeps/_workspace/src/github.com/mitchellh/go-homedir" + keystore "github.com/ipfs/go-ipfs/keystore" repo "github.com/ipfs/go-ipfs/repo" "github.com/ipfs/go-ipfs/repo/common" config "github.com/ipfs/go-ipfs/repo/config" @@ -95,6 +96,7 @@ type FSRepo struct { lockfile io.Closer config *config.Config ds repo.Datastore + keystore keystore.Keystore } var _ repo.Repo = (*FSRepo)(nil) @@ -163,6 +165,10 @@ func open(repoPath string) (repo.Repo, error) { return nil, err } + if err := r.openKeystore(); err != nil { + return nil, err + } + keepLocked = true return r, nil } @@ -303,6 +309,10 @@ func APIAddr(repoPath string) (ma.Multiaddr, error) { return ma.NewMultiaddr(s) } +func (r *FSRepo) Keystore() keystore.Keystore { + return r.keystore +} + // SetAPIAddr writes the API Addr to the /api file. func (r *FSRepo) SetAPIAddr(addr ma.Multiaddr) error { f, err := os.Create(filepath.Join(r.path, apiFile)) @@ -329,6 +339,18 @@ func (r *FSRepo) openConfig() error { return nil } +func (r *FSRepo) openKeystore() error { + ksp := filepath.Join(r.path, "keystore") + ks, err := keystore.NewFSKeystore(ksp) + if err != nil { + return err + } + + r.keystore = ks + + return nil +} + // openDatastore returns an error if the config file is not present. func (r *FSRepo) openDatastore() error { switch r.config.Datastore.Type { diff --git a/repo/mock.go b/repo/mock.go index 58ee8d2c185b..c509d08b2811 100644 --- a/repo/mock.go +++ b/repo/mock.go @@ -3,6 +3,7 @@ package repo import ( "errors" + keystore "github.com/ipfs/go-ipfs/keystore" "github.com/ipfs/go-ipfs/repo/config" ma "gx/ipfs/QmUAQaWbKxGCUTuoQVvvicbQNZ9APF5pDGWyAZSe93AtKH/go-multiaddr" @@ -14,6 +15,7 @@ var errTODO = errors.New("TODO: mock repo") type Mock struct { C config.Config D Datastore + K keystore.Keystore } func (m *Mock) Config() (*config.Config, error) { @@ -40,3 +42,5 @@ func (m *Mock) GetStorageUsage() (uint64, error) { return 0, nil } func (m *Mock) Close() error { return errTODO } func (m *Mock) SetAPIAddr(addr ma.Multiaddr) error { return errTODO } + +func (m *Mock) Keystore() keystore.Keystore { return nil } diff --git a/repo/repo.go b/repo/repo.go index a2c154ed49cd..b066632df56d 100644 --- a/repo/repo.go +++ b/repo/repo.go @@ -4,6 +4,7 @@ import ( "errors" "io" + keystore "github.com/ipfs/go-ipfs/keystore" config "github.com/ipfs/go-ipfs/repo/config" ma "gx/ipfs/QmUAQaWbKxGCUTuoQVvvicbQNZ9APF5pDGWyAZSe93AtKH/go-multiaddr" @@ -24,6 +25,8 @@ type Repo interface { Datastore() Datastore GetStorageUsage() (uint64, error) + Keystore() keystore.Keystore + // SetAPIAddr sets the API address in the repo. SetAPIAddr(addr ma.Multiaddr) error