basic keystore implementation
License: MIT Signed-off-by: Jeromy <why@ipfs.io>
Jeromy committed
Nov 30, 2016 at 10:18 UTC
805b504043ccf832056138cc32e38406e9a3d2e0
8 files changed
+381
-1
core/commands/keystore.go
new
+158
@@ -0,0 +1,158 @@
1
+package commands
2
+
3
+import (
4
+ "crypto/rand"
5
+ "fmt"
6
+ "io"
7
+ "sort"
8
+ "strings"
9
+
10
+ cmds "github.com/ipfs/go-ipfs/commands"
11
+
12
+ peer "gx/ipfs/QmfMmLGoKzCHDN7cGgk64PJr4iipzidDRME8HABSJqvmhC/go-libp2p-peer"
13
+ ci "gx/ipfs/QmfWDLQjGjVe4fr5CoztYW2DYYjRysMJrFe1RCsXLPTf46/go-libp2p-crypto"
14
+)
15
+
16
+var KeyCmd = &cmds.Command{
17
+ Helptext: cmds.HelpText{
18
+ Tagline: "Create and manipulate keypairs",
19
+ },
20
+ Subcommands: map[string]*cmds.Command{
21
+ "gen": KeyGenCmd,
22
+ "list": KeyListCmd,
23
+ },
24
+}
25
+
26
+type KeyOutput struct {
27
+ Name string
28
+ Id string
29
+}
30
+
31
+var KeyGenCmd = &cmds.Command{
32
+ Helptext: cmds.HelpText{
33
+ Tagline: "Create a new keypair",
34
+ },
35
+ Options: []cmds.Option{
36
+ cmds.StringOption("type", "t", "type of the key to create"),
37
+ cmds.IntOption("size", "s", "size of the key to generate"),
38
+ },
39
+ Arguments: []cmds.Argument{
40
+ cmds.StringArg("name", true, false, "name of key to create"),
41
+ },
42
+ Run: func(req cmds.Request, res cmds.Response) {
43
+ n, err := req.InvocContext().GetNode()
44
+ if err != nil {
45
+ res.SetError(err, cmds.ErrNormal)
46
+ return
47
+ }
48
+
49
+ typ, f, err := req.Option("type").String()
50
+ if err != nil {
51
+ res.SetError(err, cmds.ErrNormal)
52
+ return
53
+ }
54
+
55
+ if !f {
56
+ res.SetError(fmt.Errorf("please specify a key type with --type"), cmds.ErrNormal)
57
+ return
58
+ }
59
+
60
+ size, sizefound, err := req.Option("size").Int()
61
+ if err != nil {
62
+ res.SetError(err, cmds.ErrNormal)
63
+ return
64
+ }
65
+
66
+ name := req.Arguments()[0]
67
+ if name == "self" {
68
+ res.SetError(fmt.Errorf("cannot create key with name 'self'"), cmds.ErrNormal)
69
+ return
70
+ }
71
+
72
+ var sk ci.PrivKey
73
+ var pk ci.PubKey
74
+
75
+ switch typ {
76
+ case "rsa":
77
+ if !sizefound {
78
+ res.SetError(fmt.Errorf("please specify a key size with --size"), cmds.ErrNormal)
79
+ return
80
+ }
81
+
82
+ priv, pub, err := ci.GenerateKeyPairWithReader(ci.RSA, size, rand.Reader)
83
+ if err != nil {
84
+ res.SetError(err, cmds.ErrNormal)
85
+ return
86
+ }
87
+
88
+ sk = priv
89
+ pk = pub
90
+ case "ed25519":
91
+ priv, pub, err := ci.GenerateEd25519Key(rand.Reader)
92
+ if err != nil {
93
+ res.SetError(err, cmds.ErrNormal)
94
+ return
95
+ }
96
+
97
+ sk = priv
98
+ pk = pub
99
+ default:
100
+ res.SetError(fmt.Errorf("unrecognized key type: %s", typ), cmds.ErrNormal)
101
+ return
102
+ }
103
+
104
+ err = n.Repo.Keystore().Put(name, sk)
105
+ if err != nil {
106
+ res.SetError(err, cmds.ErrNormal)
107
+ return
108
+ }
109
+
110
+ pid, err := peer.IDFromPublicKey(pk)
111
+ if err != nil {
112
+ res.SetError(err, cmds.ErrNormal)
113
+ return
114
+ }
115
+
116
+ res.SetOutput(&KeyOutput{
117
+ Name: name,
118
+ Id: pid.Pretty(),
119
+ })
120
+ },
121
+ Marshalers: cmds.MarshalerMap{
122
+ cmds.Text: func(res cmds.Response) (io.Reader, error) {
123
+ k, ok := res.Output().(*KeyOutput)
124
+ if !ok {
125
+ return nil, fmt.Errorf("expected a KeyOutput as command result")
126
+ }
127
+
128
+ return strings.NewReader(k.Id), nil
129
+ },
130
+ },
131
+ Type: KeyOutput{},
132
+}
133
+
134
+var KeyListCmd = &cmds.Command{
135
+ Helptext: cmds.HelpText{
136
+ Tagline: "List all local keypairs",
137
+ },
138
+ Run: func(req cmds.Request, res cmds.Response) {
139
+ n, err := req.InvocContext().GetNode()
140
+ if err != nil {
141
+ res.SetError(err, cmds.ErrNormal)
142
+ return
143
+ }
144
+
145
+ keys, err := n.Repo.Keystore().List()
146
+ if err != nil {
147
+ res.SetError(err, cmds.ErrNormal)
148
+ return
149
+ }
150
+
151
+ sort.Strings(keys)
152
+ res.SetOutput(&stringList{keys})
153
+ },
154
+ Marshalers: cmds.MarshalerMap{
155
+ cmds.Text: stringListMarshaler,
156
+ },
157
+ Type: stringList{},
158
+}
core/commands/publish.go
+15
-1
@@ -56,6 +56,7 @@ Publish an <ipfs-path> to another public key (not implemented):
56
This accepts durations such as "300s", "1.5h" or "2h45m". Valid time units are
57
"ns", "us" (or "µs"), "ms", "s", "m", "h".`).Default("24h"),
58
cmds.StringOption("ttl", "Time duration this record should be cached for (caution: experimental)."),
59
+ cmds.StringOption("key", "k", "name of key to use").Default("self"),
60
},
61
Run: func(req cmds.Request, res cmds.Response) {
62
log.Debug("begin publish")
@@ -109,7 +110,20 @@ Publish an <ipfs-path> to another public key (not implemented):
110
ctx = context.WithValue(ctx, "ipns-publish-ttl", d)
111
}
112
112
- output, err := publish(ctx, n, n.PrivateKey, path.Path(pstr), popts)
113
+ var k crypto.PrivKey
114
+ kname, _, _ := req.Option("key").String()
115
+ if kname == "self" {
116
+ k = n.PrivateKey
117
+ } else {
118
+ ksk, err := n.Repo.Keystore().Get(kname)
119
+ if err != nil {
120
+ res.SetError(err, cmds.ErrNormal)
121
+ return
122
+ }
123
+ k = ksk
124
+ }
125
+
126
+ output, err := publish(ctx, n, k, path.Path(pstr), popts)
127
if err != nil {
128
res.SetError(err, cmds.ErrNormal)
129
return
core/commands/root.go
+1
@@ -103,6 +103,7 @@ var rootSubcommands = map[string]*cmds.Command{
103
"files": files.FilesCmd,
104
"get": GetCmd,
105
"id": IDCmd,
106
+ "key": KeyCmd,
107
"log": LogCmd,
108
"ls": LsCmd,
109
"mount": MountCmd,
keystore/keystore.go
new
+123
@@ -0,0 +1,123 @@
1
+package keystore
2
+
3
+import (
4
+ "fmt"
5
+ "io/ioutil"
6
+ "os"
7
+ "path/filepath"
8
+ "strings"
9
+
10
+ ci "gx/ipfs/QmfWDLQjGjVe4fr5CoztYW2DYYjRysMJrFe1RCsXLPTf46/go-libp2p-crypto"
11
+)
12
+
13
+type Keystore interface {
14
+ Put(string, ci.PrivKey) error
15
+ Get(string) (ci.PrivKey, error)
16
+ Delete(string) error
17
+ List() ([]string, error)
18
+}
19
+
20
+var ErrNoSuchKey = fmt.Errorf("no key by the given name was found")
21
+var ErrKeyExists = fmt.Errorf("key by that name already exists, refusing to overwrite")
22
+
23
+type FSKeystore struct {
24
+ dir string
25
+}
26
+
27
+func validateName(name string) error {
28
+ if name == "" {
29
+ return fmt.Errorf("key names must be at least one character")
30
+ }
31
+
32
+ if strings.Contains(name, "/") {
33
+ return fmt.Errorf("key names may not contain slashes")
34
+ }
35
+
36
+ if strings.HasPrefix(name, ".") {
37
+ return fmt.Errorf("key names may not begin with a period")
38
+ }
39
+
40
+ return nil
41
+}
42
+
43
+func NewFSKeystore(dir string) (*FSKeystore, error) {
44
+ _, err := os.Stat(dir)
45
+ if err != nil {
46
+ if !os.IsNotExist(err) {
47
+ return nil, err
48
+ }
49
+ if err := os.Mkdir(dir, 0700); err != nil {
50
+ return nil, err
51
+ }
52
+ }
53
+
54
+ return &FSKeystore{dir}, nil
55
+}
56
+
57
+func (ks *FSKeystore) Put(name string, k ci.PrivKey) error {
58
+ if err := validateName(name); err != nil {
59
+ return err
60
+ }
61
+
62
+ b, err := k.Bytes()
63
+ if err != nil {
64
+ return err
65
+ }
66
+
67
+ kp := filepath.Join(ks.dir, name)
68
+
69
+ _, err = os.Stat(kp)
70
+ if err == nil {
71
+ return ErrKeyExists
72
+ }
73
+
74
+ fi, err := os.Create(kp)
75
+ if err != nil {
76
+ return err
77
+ }
78
+ defer fi.Close()
79
+
80
+ _, err = fi.Write(b)
81
+ if err != nil {
82
+ return err
83
+ }
84
+
85
+ return nil
86
+}
87
+
88
+func (ks *FSKeystore) Get(name string) (ci.PrivKey, error) {
89
+ if err := validateName(name); err != nil {
90
+ return nil, err
91
+ }
92
+
93
+ kp := filepath.Join(ks.dir, name)
94
+
95
+ data, err := ioutil.ReadFile(kp)
96
+ if err != nil {
97
+ if os.IsNotExist(err) {
98
+ return nil, ErrNoSuchKey
99
+ }
100
+ return nil, err
101
+ }
102
+
103
+ return ci.UnmarshalPrivateKey(data)
104
+}
105
+
106
+func (ks *FSKeystore) Delete(name string) error {
107
+ if err := validateName(name); err != nil {
108
+ return err
109
+ }
110
+
111
+ kp := filepath.Join(ks.dir, name)
112
+
113
+ return os.Remove(kp)
114
+}
115
+
116
+func (ks *FSKeystore) List() ([]string, error) {
117
+ dir, err := os.Open(ks.dir)
118
+ if err != nil {
119
+ return nil, err
120
+ }
121
+
122
+ return dir.Readdirnames(0)
123
+}
keystore/memkeystore.go
new
+55
@@ -0,0 +1,55 @@
1
+package keystore
2
+
3
+import ci "gx/ipfs/QmfWDLQjGjVe4fr5CoztYW2DYYjRysMJrFe1RCsXLPTf46/go-libp2p-crypto"
4
+
5
+type MemKeystore struct {
6
+ keys map[string]ci.PrivKey
7
+}
8
+
9
+func NewMemKeystore() *MemKeystore {
10
+ return &MemKeystore{make(map[string]ci.PrivKey)}
11
+}
12
+
13
+func (mk *MemKeystore) Put(name string, k ci.PrivKey) error {
14
+ if err := validateName(name); err != nil {
15
+ return err
16
+ }
17
+
18
+ _, ok := mk.keys[name]
19
+ if ok {
20
+ return ErrKeyExists
21
+ }
22
+
23
+ mk.keys[name] = k
24
+ return nil
25
+}
26
+
27
+func (mk *MemKeystore) Get(name string) (ci.PrivKey, error) {
28
+ if err := validateName(name); err != nil {
29
+ return nil, err
30
+ }
31
+
32
+ k, ok := mk.keys[name]
33
+ if !ok {
34
+ return nil, ErrNoSuchKey
35
+ }
36
+
37
+ return k, nil
38
+}
39
+
40
+func (mk *MemKeystore) Delete(name string) error {
41
+ if err := validateName(name); err != nil {
42
+ return err
43
+ }
44
+
45
+ delete(mk.keys, name)
46
+ return nil
47
+}
48
+
49
+func (mk *MemKeystore) List() ([]string, error) {
50
+ out := make([]string, 0, len(mk.keys))
51
+ for k, _ := range mk.keys {
52
+ out = append(out, k)
53
+ }
54
+ return out, nil
55
+}
repo/fsrepo/fsrepo.go
+22
@@ -11,6 +11,7 @@ import (
11
"sync"
12
13
"github.com/ipfs/go-ipfs/Godeps/_workspace/src/github.com/mitchellh/go-homedir"
14
+ keystore "github.com/ipfs/go-ipfs/keystore"
15
repo "github.com/ipfs/go-ipfs/repo"
16
"github.com/ipfs/go-ipfs/repo/common"
17
config "github.com/ipfs/go-ipfs/repo/config"
@@ -95,6 +96,7 @@ type FSRepo struct {
96
lockfile io.Closer
97
config *config.Config
98
ds repo.Datastore
99
+ keystore keystore.Keystore
100
}
101
102
var _ repo.Repo = (*FSRepo)(nil)
@@ -163,6 +165,10 @@ func open(repoPath string) (repo.Repo, error) {
165
return nil, err
166
}
167
168
+ if err := r.openKeystore(); err != nil {
169
+ return nil, err
170
+ }
171
+
172
keepLocked = true
173
return r, nil
174
}
@@ -303,6 +309,10 @@ func APIAddr(repoPath string) (ma.Multiaddr, error) {
309
return ma.NewMultiaddr(s)
310
}
311
312
+func (r *FSRepo) Keystore() keystore.Keystore {
313
+ return r.keystore
314
+}
315
+
316
// SetAPIAddr writes the API Addr to the /api file.
317
func (r *FSRepo) SetAPIAddr(addr ma.Multiaddr) error {
318
f, err := os.Create(filepath.Join(r.path, apiFile))
@@ -329,6 +339,18 @@ func (r *FSRepo) openConfig() error {
339
return nil
340
}
341
342
+func (r *FSRepo) openKeystore() error {
343
+ ksp := filepath.Join(r.path, "keystore")
344
+ ks, err := keystore.NewFSKeystore(ksp)
345
+ if err != nil {
346
+ return err
347
+ }
348
+
349
+ r.keystore = ks
350
+
351
+ return nil
352
+}
353
+
354
// openDatastore returns an error if the config file is not present.
355
func (r *FSRepo) openDatastore() error {
356
switch r.config.Datastore.Type {
repo/mock.go
+4
@@ -3,6 +3,7 @@ package repo
3
import (
4
"errors"
5
6
+ keystore "github.com/ipfs/go-ipfs/keystore"
7
"github.com/ipfs/go-ipfs/repo/config"
8
9
ma "gx/ipfs/QmUAQaWbKxGCUTuoQVvvicbQNZ9APF5pDGWyAZSe93AtKH/go-multiaddr"
@@ -14,6 +15,7 @@ var errTODO = errors.New("TODO: mock repo")
15
type Mock struct {
16
C config.Config
17
D Datastore
18
+ K keystore.Keystore
19
}
20
21
func (m *Mock) Config() (*config.Config, error) {
@@ -40,3 +42,5 @@ func (m *Mock) GetStorageUsage() (uint64, error) { return 0, nil }
42
func (m *Mock) Close() error { return errTODO }
43
44
func (m *Mock) SetAPIAddr(addr ma.Multiaddr) error { return errTODO }
45
+
46
+func (m *Mock) Keystore() keystore.Keystore { return nil }
repo/repo.go
+3
@@ -4,6 +4,7 @@ import (
4
"errors"
5
"io"
6
7
+ keystore "github.com/ipfs/go-ipfs/keystore"
8
config "github.com/ipfs/go-ipfs/repo/config"
9
10
ds "gx/ipfs/QmRWDav6mzWseLWeYfVd5fvUKiVe9xNH29YfMF438fG364/go-datastore"
@@ -24,6 +25,8 @@ type Repo interface {
25
Datastore() Datastore
26
GetStorageUsage() (uint64, error)
27
28
+ Keystore() keystore.Keystore
29
+
30
// SetAPIAddr sets the API address in the repo.
31
SetAPIAddr(addr ma.Multiaddr) error
32