diff --git a/api/test/test.go b/api/test/test.go index 45622ddfd..53a92f3c4 100644 --- a/api/test/test.go +++ b/api/test/test.go @@ -2,11 +2,11 @@ package test import ( "context" - "strings" "testing" "github.com/filecoin-project/go-lotus/api" "github.com/filecoin-project/go-lotus/build" + "github.com/stretchr/testify/assert" ) // APIBuilder is a function which is invoked in test suite to provide @@ -49,9 +49,7 @@ func (ts *testSuite) testID(t *testing.T) { if err != nil { t.Fatal(err) } - if !strings.HasPrefix(id.Pretty(), "Qm") { - t.Error("expected identity to be Qm..") - } + assert.Regexp(t, "^12", id.Pretty()) } func (ts *testSuite) testConnectTwo(t *testing.T) { diff --git a/chain/types/keystore.go b/chain/types/keystore.go new file mode 100644 index 000000000..9fecf8a66 --- /dev/null +++ b/chain/types/keystore.go @@ -0,0 +1,19 @@ +package types + +// KeyInfo is used for storying keys in KeyStore +type KeyInfo struct { + Type string + PrivateKey []byte +} + +// KeyStore is used for storying secret keys +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 +} diff --git a/chain/wallet.go b/chain/wallet.go index 9ef260203..b03d37e36 100644 --- a/chain/wallet.go +++ b/chain/wallet.go @@ -4,25 +4,36 @@ import ( "encoding/binary" "fmt" "sort" + "strings" "github.com/filecoin-project/go-lotus/chain/address" + "github.com/filecoin-project/go-lotus/chain/types" "github.com/filecoin-project/go-lotus/lib/bls-signatures" "github.com/filecoin-project/go-lotus/lib/crypto" "github.com/minio/blake2b-simd" + "golang.org/x/xerrors" ) const ( + KNamePrefix = "wallet-" + KTSecp256k1 = "secp256k1" KTBLS = "bls" ) type Wallet struct { - keys map[address.Address]*KeyInfo + keys map[address.Address]*Key + keystore types.KeyStore } -func NewWallet() *Wallet { - return &Wallet{keys: make(map[address.Address]*KeyInfo)} +func NewWallet(keystore types.KeyStore) (*Wallet, error) { + w := &Wallet{ + keys: make(map[address.Address]*Key), + keystore: keystore, + } + + return w, nil } type Signature struct { @@ -118,12 +129,21 @@ func (w *Wallet) Sign(addr address.Address, msg []byte) (*Signature, error) { } } -func (w *Wallet) findKey(addr address.Address) (*KeyInfo, error) { - ki, ok := w.keys[addr] - if !ok { - return nil, fmt.Errorf("key not for given address not found in wallet") +func (w *Wallet) findKey(addr address.Address) (*Key, error) { + k, ok := w.keys[addr] + if ok { + return k, nil } - return ki, nil + ki, err := w.keystore.Get(KNamePrefix + addr.String()) + if err != nil { + return nil, xerrors.Errorf("getting from keystore: %w", err) + } + k, err = NewKey(ki) + if err != nil { + return nil, xerrors.Errorf("decoding from keystore: %w", err) + } + w.keys[k.Address] = k + return k, nil } func (w *Wallet) Export(addr address.Address) ([]byte, error) { @@ -134,74 +154,107 @@ func (w *Wallet) Import(kdata []byte) (address.Address, error) { panic("nyi") } -func (w *Wallet) ListAddrs() []address.Address { - var out []address.Address - for a := range w.keys { - out = append(out, a) +func (w *Wallet) ListAddrs() ([]address.Address, error) { + all, err := w.keystore.List() + if err != nil { + return nil, xerrors.Errorf("listing keystore: %w", err) } - sort.Slice(out, func(i, j int) bool { - return out[i].String() < out[j].String() - }) - return out + + sort.Strings(all) + + out := make([]address.Address, 0, len(all)) + for _, a := range all { + if strings.HasPrefix(a, KNamePrefix) { + name := strings.TrimPrefix(a, KNamePrefix) + addr, err := address.NewFromString(name) + if err != nil { + return nil, xerrors.Errorf("converting name to address: %w", err) + } + out = append(out, addr) + } + } + + return out, nil } func (w *Wallet) GenerateKey(typ string) (address.Address, error) { + var k *Key switch typ { case KTSecp256k1: - k, err := crypto.GenerateKey() + priv, err := crypto.GenerateKey() if err != nil { return address.Undef, err } - ki := &KeyInfo{ - PrivateKey: k, + ki := types.KeyInfo{ Type: typ, + PrivateKey: priv, } - addr := ki.Address() - w.keys[addr] = ki - return addr, nil + k, err = NewKey(ki) + if err != nil { + return address.Undef, err + } case KTBLS: priv := bls.PrivateKeyGenerate() - - ki := &KeyInfo{ + ki := types.KeyInfo{ + Type: typ, PrivateKey: priv[:], - Type: KTBLS, } - addr := ki.Address() - w.keys[addr] = ki - return addr, nil - default: - return address.Undef, fmt.Errorf("invalid key type: %s", typ) - } -} - -type KeyInfo struct { - PrivateKey []byte - - Type string -} - -func (ki *KeyInfo) Address() address.Address { - switch ki.Type { - case KTSecp256k1: - pub := crypto.PublicKey(ki.PrivateKey) - addr, err := address.NewSecp256k1Address(pub) + var err error + k, err = NewKey(ki) if err != nil { - panic(err) + return address.Undef, err + } + default: + return address.Undef, xerrors.Errorf("invalid key type: %s", typ) + } + + err := w.keystore.Put(KNamePrefix+k.Address.String(), k.KeyInfo) + if err != nil { + return address.Undef, xerrors.Errorf("saving to keystore: %w", err) + } + w.keys[k.Address] = k + return k.Address, nil +} + +type Key struct { + types.KeyInfo + + PublicKey []byte + Address address.Address +} + +func NewKey(keyinfo types.KeyInfo) (*Key, error) { + k := &Key{ + KeyInfo: keyinfo, + } + + switch k.Type { + case KTSecp256k1: + k.PublicKey = crypto.PublicKey(k.PrivateKey) + + var err error + k.Address, err = address.NewSecp256k1Address(k.PublicKey) + if err != nil { + return nil, xerrors.Errorf("converting Secp256k1 to address: %w", err) } - return addr case KTBLS: var pk bls.PrivateKey - copy(pk[:], ki.PrivateKey) + copy(pk[:], k.PrivateKey) pub := bls.PrivateKeyPublicKey(pk) - a, err := address.NewBLSAddress(pub[:]) + k.PublicKey = pub[:] + + var err error + k.Address, err = address.NewBLSAddress(k.PublicKey) if err != nil { - panic(err) + return nil, xerrors.Errorf("converting BLS to address: %w", err) } - return a + default: - panic("unsupported key type") + return nil, xerrors.Errorf("unknown key type") } + return k, nil + } 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/api.go b/node/api.go index 9c58d3041..b164fef33 100644 --- a/node/api.go +++ b/node/api.go @@ -110,7 +110,7 @@ func (a *API) WalletNew(ctx context.Context, typ string) (address.Address, error } func (a *API) WalletList(ctx context.Context) ([]address.Address, error) { - return a.Wallet.ListAddrs(), nil + return a.Wallet.ListAddrs() } func (a *API) NetConnect(ctx context.Context, p peer.AddrInfo) error { diff --git a/node/builder.go b/node/builder.go index 3a595147b..b9d98176b 100644 --- a/node/builder.go +++ b/node/builder.go @@ -24,6 +24,7 @@ import ( "github.com/filecoin-project/go-lotus/api" "github.com/filecoin-project/go-lotus/chain" + "github.com/filecoin-project/go-lotus/chain/types" "github.com/filecoin-project/go-lotus/node/config" "github.com/filecoin-project/go-lotus/node/hello" "github.com/filecoin-project/go-lotus/node/modules" @@ -115,11 +116,6 @@ var defConf = config.Default() func defaults() []Option { return []Option{ Override(new(helpers.MetricsCtx), context.Background), - - randomIdentity(), - - Override(new(datastore.Batching), testing.MapDatastore), - Override(new(blockstore.Blockstore), testing.MapBlockstore), // NOT on top of ds above Override(new(record.Validator), modules.RecordValidator), // Filecoin modules @@ -227,6 +223,8 @@ func Repo(r repo.Repo) Option { Override(new(ci.PrivKey), pk), Override(new(ci.PubKey), ci.PrivKey.GetPublic), Override(new(peer.ID), peer.IDFromPublicKey), + + Override(new(types.KeyStore), modules.KeyStore), ) } diff --git a/node/modules/core.go b/node/modules/core.go index 25e29db8b..12ea05bdf 100644 --- a/node/modules/core.go +++ b/node/modules/core.go @@ -23,6 +23,7 @@ import ( "go.uber.org/fx" "github.com/filecoin-project/go-lotus/chain" + "github.com/filecoin-project/go-lotus/chain/types" "github.com/filecoin-project/go-lotus/node/modules/helpers" "github.com/filecoin-project/go-lotus/node/repo" ) @@ -65,6 +66,10 @@ func LockedRepo(lr repo.LockedRepo) func(lc fx.Lifecycle) repo.LockedRepo { } } +func KeyStore(lr repo.LockedRepo) (types.KeyStore, error) { + return lr.KeyStore() +} + func Datastore(r repo.LockedRepo) (datastore.Batching, error) { return r.Datastore("/metadata") } diff --git a/node/node_test.go b/node/node_test.go index fb1d348c2..708065d6d 100644 --- a/node/node_test.go +++ b/node/node_test.go @@ -12,6 +12,7 @@ import ( "github.com/filecoin-project/go-lotus/api/test" "github.com/filecoin-project/go-lotus/lib/jsonrpc" + "github.com/filecoin-project/go-lotus/node/repo" mocknet "github.com/libp2p/go-libp2p/p2p/net/mock" ) @@ -25,6 +26,7 @@ func builder(t *testing.T, n int) []api.API { var err error out[i], err = node.New(ctx, node.Online(), + node.Repo(repo.NewMemory(nil)), MockHost(mn), ) if err != nil { diff --git a/node/repo/fsrepo.go b/node/repo/fsrepo.go index b72160b93..fa1dd2951 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,10 +16,12 @@ 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" + "github.com/filecoin-project/go-lotus/chain/types" "github.com/filecoin-project/go-lotus/node/config" ) @@ -28,6 +31,7 @@ const ( fsDatastore = "datastore" fsLibp2pKey = "libp2p.priv" fsLock = "repo.lock" + fsKeystore = "keystore" ) var log = logging.Logger("repo") @@ -55,14 +59,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 +187,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 +245,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() (types.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 types.KeyInfo coresponding to named key +func (fsr *fsLockedRepo) Get(name string) (types.KeyInfo, error) { + if err := fsr.stillValid(); err != nil { + return types.KeyInfo{}, err + } + + encName := base32.RawStdEncoding.EncodeToString([]byte(name)) + keyPath := fsr.join(fsKeystore, encName) + + fstat, err := os.Stat(keyPath) + if os.IsNotExist(err) { + return types.KeyInfo{}, xerrors.Errorf("opening key '%s': %w", name, ErrKeyNotFound) + } else if err != nil { + return types.KeyInfo{}, xerrors.Errorf("opening key '%s': %w", name, err) + } + + if fstat.Mode()&0077 != 0 { + return types.KeyInfo{}, xerrors.Errorf(kstrPermissionMsg, name, err) + } + + file, err := os.Open(keyPath) + if err != nil { + return types.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 types.KeyInfo{}, xerrors.Errorf("reading key '%s': %w", name, err) + } + + var res types.KeyInfo + err = json.Unmarshal(data, &res) + if err != nil { + return types.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 types.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..47e55e10f 100644 --- a/node/repo/interface.go +++ b/node/repo/interface.go @@ -1,18 +1,23 @@ 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/chain/types" "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 { @@ -40,8 +45,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() (types.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..5028074f2 100644 --- a/node/repo/memrepo.go +++ b/node/repo/memrepo.go @@ -9,7 +9,9 @@ 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/chain/types" "github.com/filecoin-project/go-lotus/node/config" ) @@ -25,7 +27,7 @@ type MemRepo struct { datastore datastore.Datastore configF func() *config.Root libp2pKey crypto.PrivKey - wallet interface{} + keystore map[string]types.KeyInfo } type lockedMemRepo struct { @@ -46,7 +48,7 @@ type MemRepoOptions struct { Ds datastore.Datastore ConfigF func() *config.Root Libp2pKey crypto.PrivKey - Wallet interface{} + KeyStore map[string]types.KeyInfo } func genLibp2pKey() (crypto.PrivKey, error) { @@ -77,6 +79,9 @@ func NewMemory(opts *MemRepoOptions) *MemRepo { } opts.Libp2pKey = pk } + if opts.KeyStore == nil { + opts.KeyStore = make(map[string]types.KeyInfo) + } return &MemRepo{ repoLock: make(chan struct{}, 1), @@ -84,7 +89,7 @@ func NewMemory(opts *MemRepoOptions) *MemRepo { datastore: opts.Ds, configF: opts.ConfigF, libp2pKey: opts.Libp2pKey, - wallet: opts.Wallet, + keystore: opts.KeyStore, } } @@ -172,9 +177,73 @@ func (lmem *lockedMemRepo) SetAPIEndpoint(ma multiaddr.Multiaddr) error { return nil } -func (lmem *lockedMemRepo) Wallet() (interface{}, error) { +func (lmem *lockedMemRepo) KeyStore() (types.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 types.KeyInfo coresponding to named key +func (lmem *lockedMemRepo) Get(name string) (types.KeyInfo, error) { + if err := lmem.checkToken(); err != nil { + return types.KeyInfo{}, err + } + lmem.RLock() + defer lmem.RUnlock() + + key, ok := lmem.mem.keystore[name] + if !ok { + return types.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 types.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..d12c5e953 100644 --- a/node/repo/repo_test.go +++ b/node/repo/repo_test.go @@ -5,7 +5,9 @@ import ( "github.com/multiformats/go-multiaddr" "github.com/stretchr/testify/assert" + "golang.org/x/xerrors" + "github.com/filecoin-project/go-lotus/chain/types" "github.com/filecoin-project/go-lotus/node/config" ) @@ -62,4 +64,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 := types.KeyInfo{Type: "foo"} + k2 := types.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") + } }