Skip to content

Commit fee2b7f

Browse files
committed
basic keystore implementation
License: MIT Signed-off-by: Jeromy <[email protected]>
1 parent 4511a4a commit fee2b7f

File tree

8 files changed

+383
-3
lines changed

8 files changed

+383
-3
lines changed

core/commands/keystore.go

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
package commands
22

33
import (
4+
"context"
45
"errors"
56
"fmt"
67
"io"
78
"strings"
89
"time"
910

10-
context "context"
11-
1211
cmds "github.com/ipfs/go-ipfs/commands"
1312
core "github.com/ipfs/go-ipfs/core"
1413
path "github.com/ipfs/go-ipfs/path"
14+
1515
key "gx/ipfs/QmYEoKZXHoAToWfhGF3vryhMn3WWhE1o2MasQ8uzY5iDi9/go-key"
1616
crypto "gx/ipfs/QmfWDLQjGjVe4fr5CoztYW2DYYjRysMJrFe1RCsXLPTf46/go-libp2p-crypto"
1717
)
@@ -56,6 +56,7 @@ Publish an <ipfs-path> to another public key (not implemented):
5656
This accepts durations such as "300s", "1.5h" or "2h45m". Valid time units are
5757
"ns", "us" (or "µs"), "ms", "s", "m", "h".`).Default("24h"),
5858
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"),
5960
},
6061
Run: func(req cmds.Request, res cmds.Response) {
6162
log.Debug("begin publish")
@@ -109,7 +110,20 @@ Publish an <ipfs-path> to another public key (not implemented):
109110
ctx = context.WithValue(ctx, "ipns-publish-ttl", d)
110111
}
111112

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)
113127
if err != nil {
114128
res.SetError(err, cmds.ErrNormal)
115129
return

core/commands/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ var rootSubcommands = map[string]*cmds.Command{
103103
"files": files.FilesCmd,
104104
"get": GetCmd,
105105
"id": IDCmd,
106+
"key": KeyCmd,
106107
"log": LogCmd,
107108
"ls": LsCmd,
108109
"mount": MountCmd,

keystore/keystore.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
}

0 commit comments

Comments
 (0)