diff --git a/go.mod b/go.mod index d5817cdc3..9e9bbebbc 100644 --- a/go.mod +++ b/go.mod @@ -48,6 +48,7 @@ require ( github.com/libp2p/go-maddr-filter v0.0.5 github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1 github.com/mitchellh/go-homedir v1.1.0 + github.com/multiformats/go-base32 v0.0.3 github.com/multiformats/go-multiaddr v0.0.4 github.com/multiformats/go-multiaddr-dns v0.0.3 github.com/multiformats/go-multiaddr-net v0.0.1 diff --git a/node/repo/fsrepo.go b/node/repo/fsrepo.go index b72160b93..21f51166e 100644 --- a/node/repo/fsrepo.go +++ b/node/repo/fsrepo.go @@ -1,6 +1,7 @@ package repo import ( + "encoding/json" "io" "io/ioutil" "os" @@ -15,6 +16,7 @@ import ( logging "github.com/ipfs/go-log" "github.com/libp2p/go-libp2p-core/crypto" "github.com/mitchellh/go-homedir" + "github.com/multiformats/go-base32" "github.com/multiformats/go-multiaddr" "github.com/pkg/errors" "golang.org/x/xerrors" @@ -28,6 +30,7 @@ const ( fsDatastore = "datastore" fsLibp2pKey = "libp2p.priv" fsLock = "repo.lock" + fsKeystore = "keystore" ) var log = logging.Logger("repo") @@ -55,14 +58,28 @@ func NewFS(path string) (*FsRepo, error) { func (fsr *FsRepo) Init() error { if _, err := os.Stat(fsr.path); err == nil { - return ErrRepoExists + return fsr.initKeystore() } else if !os.IsNotExist(err) { return err } log.Infof("Initializing repo at '%s'", fsr.path) + err := os.Mkdir(fsr.path, 0755) //nolint: gosec + if err != nil { + return err + } + return fsr.initKeystore() - return os.Mkdir(fsr.path, 0755) //nolint: gosec +} + +func (fsr *FsRepo) initKeystore() error { + kstorePath := filepath.Join(fsr.path, fsKeystore) + if _, err := os.Stat(kstorePath); err == nil { + return ErrRepoExists + } else if !os.IsNotExist(err) { + return err + } + return os.Mkdir(kstorePath, 0700) } // APIEndpoint returns endpoint of API in this repo @@ -169,6 +186,9 @@ func (fsr *fsLockedRepo) Config() (*config.Root, error) { } func (fsr *fsLockedRepo) Libp2pIdentity() (crypto.PrivKey, error) { + if err := fsr.stillValid(); err != nil { + return nil, err + } kpath := fsr.join(fsLibp2pKey) stat, err := os.Stat(kpath) @@ -224,6 +244,131 @@ func (fsr *fsLockedRepo) SetAPIEndpoint(ma multiaddr.Multiaddr) error { return ioutil.WriteFile(fsr.join(fsAPI), []byte(ma.String()), 0644) } -func (fsr *fsLockedRepo) Wallet() (interface{}, error) { - panic("not implemented") +func (fsr *fsLockedRepo) KeyStore() (KeyStore, error) { + if err := fsr.stillValid(); err != nil { + return nil, err + } + return fsr, nil +} + +var kstrPermissionMsg = "permissions of key: '%s' are too relaxed, " + + "required: 0600, got: %#o" + +// List lists all the keys stored in the KeyStore +func (fsr *fsLockedRepo) List() ([]string, error) { + if err := fsr.stillValid(); err != nil { + return nil, err + } + + kstorePath := fsr.join(fsKeystore) + dir, err := os.Open(kstorePath) + if err != nil { + return nil, xerrors.Errorf("opening dir to list keystore: %w", err) + } + files, err := dir.Readdir(-1) + if err != nil { + return nil, xerrors.Errorf("reading keystore dir: %w", err) + } + keys := make([]string, 0, len(files)) + for _, f := range files { + if f.Mode()&0077 != 0 { + return nil, xerrors.Errorf(kstrPermissionMsg, f.Name(), f.Mode()) + } + name, err := base32.RawStdEncoding.DecodeString(f.Name()) + if err != nil { + return nil, xerrors.Errorf("decoding key: '%s': %w", f.Name(), err) + } + keys = append(keys, string(name)) + } + return keys, nil +} + +// Get gets a key out of keystore and returns KeyInfo coresponding to named key +func (fsr *fsLockedRepo) Get(name string) (KeyInfo, error) { + if err := fsr.stillValid(); err != nil { + return KeyInfo{}, err + } + + encName := base32.RawStdEncoding.EncodeToString([]byte(name)) + keyPath := fsr.join(fsKeystore, encName) + + fstat, err := os.Stat(keyPath) + if os.IsNotExist(err) { + return KeyInfo{}, xerrors.Errorf("opening key '%s': %w", name, ErrKeyNotFound) + } else if err != nil { + return KeyInfo{}, xerrors.Errorf("opening key '%s': %w", name, err) + } + + if fstat.Mode()&0077 != 0 { + return KeyInfo{}, xerrors.Errorf(kstrPermissionMsg, name, err) + } + + file, err := os.Open(keyPath) + if err != nil { + return KeyInfo{}, xerrors.Errorf("opening key '%s': %w", name, err) + } + defer file.Close() //nolint: errcheck // read only op + + data, err := ioutil.ReadAll(file) + if err != nil { + return KeyInfo{}, xerrors.Errorf("reading key '%s': %w", name, err) + } + + var res KeyInfo + err = json.Unmarshal(data, &res) + if err != nil { + return KeyInfo{}, xerrors.Errorf("decoding key '%s': %w", name, err) + } + + return res, nil +} + +// Put saves key info under given name +func (fsr *fsLockedRepo) Put(name string, info KeyInfo) error { + if err := fsr.stillValid(); err != nil { + return err + } + + encName := base32.RawStdEncoding.EncodeToString([]byte(name)) + keyPath := fsr.join(fsKeystore, encName) + + _, err := os.Stat(keyPath) + if err == nil { + return xerrors.Errorf("checking key before put '%s': %w", name, ErrKeyExists) + } else if !os.IsNotExist(err) { + return xerrors.Errorf("checking key before put '%s': %w", name, err) + } + + keyData, err := json.Marshal(info) + if err != nil { + return xerrors.Errorf("encoding key '%s': %w", name, err) + } + + err = ioutil.WriteFile(keyPath, keyData, 0600) + if err != nil { + return xerrors.Errorf("writing key '%s': %w", name, err) + } + return nil +} + +func (fsr *fsLockedRepo) Delete(name string) error { + if err := fsr.stillValid(); err != nil { + return err + } + + encName := base32.RawStdEncoding.EncodeToString([]byte(name)) + keyPath := fsr.join(fsKeystore, encName) + + _, err := os.Stat(keyPath) + if os.IsNotExist(err) { + return xerrors.Errorf("checking key before delete '%s': %w", name, ErrKeyNotFound) + } else if err != nil { + return xerrors.Errorf("checking key before delete '%s': %w", name, err) + } + + err = os.Remove(keyPath) + if err != nil { + return xerrors.Errorf("deleting key '%s': %w", name, err) + } + return nil } diff --git a/node/repo/fsrepo_test.go b/node/repo/fsrepo_test.go index 4cda09f4e..938314a23 100644 --- a/node/repo/fsrepo_test.go +++ b/node/repo/fsrepo_test.go @@ -1,11 +1,13 @@ package repo -import "io/ioutil" -import "os" -import "testing" +import ( + "io/ioutil" + "os" + "testing" +) func genFsRepo(t *testing.T) (*FsRepo, func()) { - path, err := ioutil.TempDir("", "lotus-repo-*") + path, err := ioutil.TempDir("", "lotus-repo-") if err != nil { t.Fatal(err) } @@ -14,6 +16,11 @@ func genFsRepo(t *testing.T) (*FsRepo, func()) { if err != nil { t.Fatal(err) } + + err = repo.Init() + if err != ErrRepoExists && err != nil { + t.Fatal(err) + } return repo, func() { _ = os.RemoveAll(path) } diff --git a/node/repo/interface.go b/node/repo/interface.go index bd321f73a..0c99df1d2 100644 --- a/node/repo/interface.go +++ b/node/repo/interface.go @@ -1,18 +1,22 @@ package repo import ( + "errors" + "github.com/ipfs/go-datastore" "github.com/libp2p/go-libp2p-core/crypto" "github.com/multiformats/go-multiaddr" - "golang.org/x/xerrors" "github.com/filecoin-project/go-lotus/node/config" ) var ( - ErrNoAPIEndpoint = xerrors.New("API not running (no endpoint)") - ErrRepoAlreadyLocked = xerrors.New("repo is already locked") - ErrClosedRepo = xerrors.New("repo is no longer open") + ErrNoAPIEndpoint = errors.New("API not running (no endpoint)") + ErrRepoAlreadyLocked = errors.New("repo is already locked") + ErrClosedRepo = errors.New("repo is no longer open") + + ErrKeyExists = errors.New("key already exists") + ErrKeyNotFound = errors.New("key not found") ) type Repo interface { @@ -23,6 +27,22 @@ type Repo interface { Lock() (LockedRepo, error) } +type KeyInfo struct { + Type string + PrivateKey []byte +} + +type KeyStore interface { + // List lists all the keys stored in the KeyStore + List() ([]string, error) + // Get gets a key out of keystore and returns KeyInfo coresponding to named key + Get(string) (KeyInfo, error) + // Put saves a key info under given name + Put(string, KeyInfo) error + // Delete removes a key from keystore + Delete(string) error +} + type LockedRepo interface { // Close closes repo and removes lock. Close() error @@ -40,8 +60,8 @@ type LockedRepo interface { // so it can be read by API clients SetAPIEndpoint(multiaddr.Multiaddr) error - // Wallet returns store of private keys for Filecoin transactions - Wallet() (interface{}, error) + // KeyStore returns store of private keys for Filecoin transactions + KeyStore() (KeyStore, error) // Path returns absolute path of the repo (or empty string if in-memory) Path() string diff --git a/node/repo/memrepo.go b/node/repo/memrepo.go index f5b466ad3..35361e90a 100644 --- a/node/repo/memrepo.go +++ b/node/repo/memrepo.go @@ -9,6 +9,7 @@ import ( dssync "github.com/ipfs/go-datastore/sync" "github.com/libp2p/go-libp2p-core/crypto" "github.com/multiformats/go-multiaddr" + "golang.org/x/xerrors" "github.com/filecoin-project/go-lotus/node/config" ) @@ -25,7 +26,7 @@ type MemRepo struct { datastore datastore.Datastore configF func() *config.Root libp2pKey crypto.PrivKey - wallet interface{} + keystore map[string]KeyInfo } type lockedMemRepo struct { @@ -46,7 +47,7 @@ type MemRepoOptions struct { Ds datastore.Datastore ConfigF func() *config.Root Libp2pKey crypto.PrivKey - Wallet interface{} + KeyStore map[string]KeyInfo } func genLibp2pKey() (crypto.PrivKey, error) { @@ -77,6 +78,9 @@ func NewMemory(opts *MemRepoOptions) *MemRepo { } opts.Libp2pKey = pk } + if opts.KeyStore == nil { + opts.KeyStore = make(map[string]KeyInfo) + } return &MemRepo{ repoLock: make(chan struct{}, 1), @@ -84,7 +88,7 @@ func NewMemory(opts *MemRepoOptions) *MemRepo { datastore: opts.Ds, configF: opts.ConfigF, libp2pKey: opts.Libp2pKey, - wallet: opts.Wallet, + keystore: opts.KeyStore, } } @@ -172,9 +176,73 @@ func (lmem *lockedMemRepo) SetAPIEndpoint(ma multiaddr.Multiaddr) error { return nil } -func (lmem *lockedMemRepo) Wallet() (interface{}, error) { +func (lmem *lockedMemRepo) KeyStore() (KeyStore, error) { if err := lmem.checkToken(); err != nil { return nil, err } - return lmem.mem.wallet, nil + return lmem, nil +} + +// Implement KeyStore on the same instance + +// List lists all the keys stored in the KeyStore +func (lmem *lockedMemRepo) List() ([]string, error) { + if err := lmem.checkToken(); err != nil { + return nil, err + } + lmem.RLock() + defer lmem.RUnlock() + + res := make([]string, 0, len(lmem.mem.keystore)) + for k := range lmem.mem.keystore { + res = append(res, k) + } + return res, nil +} + +// Get gets a key out of keystore and returns KeyInfo coresponding to named key +func (lmem *lockedMemRepo) Get(name string) (KeyInfo, error) { + if err := lmem.checkToken(); err != nil { + return KeyInfo{}, err + } + lmem.RLock() + defer lmem.RUnlock() + + key, ok := lmem.mem.keystore[name] + if !ok { + return KeyInfo{}, xerrors.Errorf("getting key '%s': %w", name, ErrKeyNotFound) + } + return key, nil +} + +// Put saves key info under given name +func (lmem *lockedMemRepo) Put(name string, key KeyInfo) error { + if err := lmem.checkToken(); err != nil { + return err + } + lmem.Lock() + defer lmem.Unlock() + + _, isThere := lmem.mem.keystore[name] + if isThere { + return xerrors.Errorf("putting key '%s': %w", name, ErrKeyExists) + } + + lmem.mem.keystore[name] = key + return nil +} + +func (lmem *lockedMemRepo) Delete(name string) error { + if err := lmem.checkToken(); err != nil { + return err + } + lmem.Lock() + defer lmem.Unlock() + + _, isThere := lmem.mem.keystore[name] + if !isThere { + return xerrors.Errorf("deleting key '%s': %w", name, ErrKeyNotFound) + } + delete(lmem.mem.keystore, name) + return nil } diff --git a/node/repo/repo_test.go b/node/repo/repo_test.go index 2e39e1dcc..de0fcf889 100644 --- a/node/repo/repo_test.go +++ b/node/repo/repo_test.go @@ -5,6 +5,7 @@ import ( "github.com/multiformats/go-multiaddr" "github.com/stretchr/testify/assert" + "golang.org/x/xerrors" "github.com/filecoin-project/go-lotus/node/config" ) @@ -62,4 +63,56 @@ func basicTest(t *testing.T, repo Repo) { assert.Equal(t, ErrNoAPIEndpoint, err, "after closing repo, api should be nil") } assert.Nil(t, apima, "with closed repo, apima should be set back to nil") + + k1 := KeyInfo{Type: "foo"} + k2 := KeyInfo{Type: "bar"} + + lrepo, err = repo.Lock() + assert.NoError(t, err, "should be able to relock") + assert.NotNil(t, lrepo, "locked repo shouldn't be nil") + + kstr, err := lrepo.KeyStore() + assert.NoError(t, err, "should be able to get keystore") + assert.NotNil(t, lrepo, "keystore shouldn't be nil") + + list, err := kstr.List() + assert.NoError(t, err, "should be able to list key") + assert.Empty(t, list, "there should be no keys") + + err = kstr.Put("k1", k1) + assert.NoError(t, err, "should be able to put k1") + + err = kstr.Put("k1", k1) + if assert.Error(t, err, "putting key under the same name should error") { + assert.True(t, xerrors.Is(err, ErrKeyExists), "returned error is ErrKeyExists") + } + + k1prim, err := kstr.Get("k1") + assert.NoError(t, err, "should be able to get k1") + assert.Equal(t, k1, k1prim, "returned key should be the same") + + k2prim, err := kstr.Get("k2") + if assert.Error(t, err, "should not be able to get k2") { + assert.True(t, xerrors.Is(err, ErrKeyNotFound), "returned error is ErrKeyNotFound") + } + assert.Empty(t, k2prim, "there should be no output for k2") + + err = kstr.Put("k2", k2) + assert.NoError(t, err, "should be able to put k2") + + list, err = kstr.List() + assert.NoError(t, err, "should be able to list keys") + assert.ElementsMatch(t, []string{"k1", "k2"}, list, "returned elements match") + + err = kstr.Delete("k2") + assert.NoError(t, err, "should be able to delete key") + + list, err = kstr.List() + assert.NoError(t, err, "should be able to list keys") + assert.ElementsMatch(t, []string{"k1"}, list, "returned elements match") + + err = kstr.Delete("k2") + if assert.Error(t, err) { + assert.True(t, xerrors.Is(err, ErrKeyNotFound), "returned errror is ErrKeyNotFound") + } }