diff --git a/dscache/convert_test.go b/dscache/convert_test.go index b02f09403..13eef6bf5 100644 --- a/dscache/convert_test.go +++ b/dscache/convert_test.go @@ -11,6 +11,7 @@ import ( "github.com/qri-io/qfs" testPeers "github.com/qri-io/qri/config/test" "github.com/qri-io/qri/dsref" + "github.com/qri-io/qri/key" "github.com/qri-io/qri/logbook" "github.com/qri-io/qri/profile" reporef "github.com/qri-io/qri/repo/ref" @@ -440,7 +441,11 @@ func TestBuildDscacheFromLogbookAndProfilesAndDsrefAlphabetized(t *testing.T) { Peername: "test_user", PrivKey: peerInfo.PrivKey, } - profiles, err := profile.NewMemStore(pro) + keyStore, err := key.NewMemStore() + if err != nil { + t.Fatal(err) + } + profiles, err := profile.NewMemStore(pro, keyStore) if err != nil { t.Fatal(err) } @@ -513,7 +518,11 @@ func TestBuildDscacheFromLogbookAndProfilesAndDsrefFillInfo(t *testing.T) { Peername: "test_user", PrivKey: peerInfo.PrivKey, } - profiles, err := profile.NewMemStore(pro) + keyStore, err := key.NewMemStore() + if err != nil { + t.Fatal(err) + } + profiles, err := profile.NewMemStore(pro, keyStore) if err != nil { t.Fatal(err) } diff --git a/key/key.go b/key/key.go new file mode 100644 index 000000000..b8e4ca657 --- /dev/null +++ b/key/key.go @@ -0,0 +1,10 @@ +package key + +import ( + logger "github.com/ipfs/go-log" +) + +var log = logger.Logger("key") + +// ID is the key identifier +type ID string diff --git a/key/keybook.go b/key/keybook.go new file mode 100644 index 000000000..f66314b99 --- /dev/null +++ b/key/keybook.go @@ -0,0 +1,178 @@ +package key + +import ( + "encoding/json" + "errors" + "sync" + + ic "github.com/libp2p/go-libp2p-core/crypto" +) + +// Book defines the interface for keybook implementations +// which hold the key information +type Book interface { + // PubKey stores the public key for a key.ID + PubKey(ID) ic.PubKey + + // AddPubKey stores the public for a key.ID + AddPubKey(ID, ic.PubKey) error + + // PrivKey returns the private key for a key.ID, if known + PrivKey(ID) ic.PrivKey + + // AddPrivKey stores the private key for a key.ID + AddPrivKey(ID, ic.PrivKey) error + + // IDsWithKeys returns all the key IDs stored in the KeyBook + IDsWithKeys() []ID +} + +type memoryKeyBook struct { + sync.RWMutex // same lock. wont happen a ton. + pks map[ID]ic.PubKey + sks map[ID]ic.PrivKey +} + +var _ Book = (*memoryKeyBook)(nil) + +func newKeyBook() *memoryKeyBook { + return &memoryKeyBook{ + pks: map[ID]ic.PubKey{}, + sks: map[ID]ic.PrivKey{}, + } +} + +// IDsWithKeys returns the list of IDs in the KeyBook +func (mkb *memoryKeyBook) IDsWithKeys() []ID { + mkb.RLock() + ps := make([]ID, 0, len(mkb.pks)+len(mkb.sks)) + for p := range mkb.pks { + ps = append(ps, p) + } + for p := range mkb.sks { + if _, found := mkb.pks[p]; !found { + ps = append(ps, p) + } + } + mkb.RUnlock() + return ps +} + +// PubKey returns the public key for a given ID if it exists +func (mkb *memoryKeyBook) PubKey(k ID) ic.PubKey { + mkb.RLock() + pk := mkb.pks[k] + mkb.RUnlock() + // TODO(arqu): we ignore the recovery mechanic to avoid magic + // behavior in above stores. We should revisit once we work out + // the broader mechanics of managing keys. + // pk, _ = p.ExtractPublicKey() + // if err == nil { + // mkb.Lock() + // mkb.pks[p] = pk + // mkb.Unlock() + // } + return pk +} + +// AddPubKey inserts a public key for a given ID +func (mkb *memoryKeyBook) AddPubKey(k ID, pk ic.PubKey) error { + mkb.Lock() + mkb.pks[k] = pk + mkb.Unlock() + return nil +} + +// PrivKey returns the private key for a given ID if it exists +func (mkb *memoryKeyBook) PrivKey(k ID) ic.PrivKey { + mkb.RLock() + sk := mkb.sks[k] + mkb.RUnlock() + return sk +} + +// AddPrivKey inserts a private key for a given ID +func (mkb *memoryKeyBook) AddPrivKey(k ID, sk ic.PrivKey) error { + if sk == nil { + return errors.New("sk is nil (PrivKey)") + } + + mkb.Lock() + mkb.sks[k] = sk + mkb.Unlock() + return nil +} + +// MarshalJSON implements the JSON marshal interface +func (mkb *memoryKeyBook) MarshalJSON() ([]byte, error) { + mkb.RLock() + res := map[string]interface{}{} + pubKeys := map[ID]string{} + privKeys := map[ID]string{} + for k, v := range mkb.pks { + byteKey, err := ic.MarshalPublicKey(v) + if err != nil { + // skip/don't marshal ill formed keys + log.Debugf("keybook: failed to marshal key: %q", err.Error()) + continue + } + pubKeys[k] = ic.ConfigEncodeKey(byteKey) + } + for k, v := range mkb.sks { + byteKey, err := ic.MarshalPrivateKey(v) + if err != nil { + // skip/don't marshal ill formed keys + log.Debugf("keybook: failed to marshal key: %q", err.Error()) + continue + } + privKeys[k] = ic.ConfigEncodeKey(byteKey) + } + + res["public_keys"] = pubKeys + res["private_keys"] = privKeys + + mkb.RUnlock() + return json.Marshal(res) +} + +// UnmarshalJSON implements the JSON unmarshal interface +func (mkb *memoryKeyBook) UnmarshalJSON(data []byte) error { + keyBookJSON := map[string]map[string]string{} + err := json.Unmarshal(data, &keyBookJSON) + if err != nil { + return err + } + if pubKeys, ok := keyBookJSON["public_keys"]; ok { + for k, v := range pubKeys { + byteKey, err := ic.ConfigDecodeKey(v) + if err != nil { + return err + } + key, err := ic.UnmarshalPublicKey(byteKey) + if err != nil { + return err + } + err = mkb.AddPubKey(ID(k), key) + if err != nil { + return err + } + } + } + if privKeys, ok := keyBookJSON["private_keys"]; ok { + for k, v := range privKeys { + byteKey, err := ic.ConfigDecodeKey(v) + if err != nil { + return err + } + key, err := ic.UnmarshalPrivateKey(byteKey) + if err != nil { + return err + } + err = mkb.AddPrivKey(ID(k), key) + if err != nil { + return err + } + } + } + return nil +} diff --git a/key/keybook_test.go b/key/keybook_test.go new file mode 100644 index 000000000..5b2ce410f --- /dev/null +++ b/key/keybook_test.go @@ -0,0 +1,93 @@ +package key + +import ( + "testing" + + cfgtest "github.com/qri-io/qri/config/test" +) + +func TestPublicKey(t *testing.T) { + kb := newKeyBook() + pi0 := cfgtest.GetTestPeerInfo(0) + k0 := ID("key_id_0") + + err := kb.AddPubKey(k0, pi0.PubKey) + if err != nil { + t.Fatal(err) + } + + pi1 := cfgtest.GetTestPeerInfo(1) + k1 := ID("key_id_1") + err = kb.AddPubKey(k1, pi1.PubKey) + if err != nil { + t.Fatal(err) + } + + tPub := kb.PubKey(k0) + if tPub != pi0.PubKey { + t.Fatalf("returned key does not match") + } + + tPub = kb.PubKey(k1) + if tPub != pi1.PubKey { + t.Fatalf("returned key does not match") + } +} + +func TestPrivateKey(t *testing.T) { + kb := newKeyBook() + pi0 := cfgtest.GetTestPeerInfo(0) + k0 := ID("key_id_0") + + err := kb.AddPrivKey(k0, pi0.PrivKey) + if err != nil { + t.Fatal(err) + } + + pi1 := cfgtest.GetTestPeerInfo(1) + k1 := ID("key_id_1") + err = kb.AddPrivKey(k1, pi1.PrivKey) + if err != nil { + t.Fatal(err) + } + + tPriv := kb.PrivKey(k0) + if tPriv != pi0.PrivKey { + t.Fatalf("returned key does not match") + } + + tPriv = kb.PrivKey(k1) + if tPriv != pi1.PrivKey { + t.Fatalf("returned key does not match") + } +} + +func TestIDsWithKeys(t *testing.T) { + kb := newKeyBook() + pi0 := cfgtest.GetTestPeerInfo(0) + k0 := ID("key_id_0") + + err := kb.AddPrivKey(k0, pi0.PrivKey) + if err != nil { + t.Fatal(err) + } + + pi1 := cfgtest.GetTestPeerInfo(1) + k1 := ID("key_id_1") + err = kb.AddPubKey(k1, pi1.PubKey) + if err != nil { + t.Fatal(err) + } + + pids := kb.IDsWithKeys() + + if len(pids) != 2 { + t.Fatalf("expected to get 2 ids but got: %d", len(pids)) + } + + // the output of kb.IDsWithKeys is in a non-deterministic order + // so we have to account for all permutations + if !(pids[0] == k0 && pids[1] == k1) && !(pids[0] == k1 && pids[1] == k0) { + t.Fatalf("invalid ids returned") + } +} diff --git a/key/store.go b/key/store.go new file mode 100644 index 000000000..33676610b --- /dev/null +++ b/key/store.go @@ -0,0 +1,184 @@ +package key + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "sync" + + "github.com/libp2p/go-libp2p-core/crypto" + "github.com/qri-io/qri/config" + "github.com/theckman/go-flock" +) + +// Store is an abstraction over a KeyBook +// In the future we may expand this interface to store symmetric encryption keys +type Store interface { + Book +} + +// NewStore constructs a keys.Store backed by memory or local file +func NewStore(cfg *config.Config) (Store, error) { + if cfg.Repo == nil { + return NewMemStore() + } + + switch cfg.Repo.Type { + case "fs": + return NewLocalStore(filepath.Join(filepath.Dir(cfg.Path()), "keystore.json")) + case "mem": + return NewMemStore() + default: + return nil, fmt.Errorf("unknown repo type: %s", cfg.Repo.Type) + } +} + +type memStore struct { + Book +} + +// NewMemStore constructs an in-memory key.Store +func NewMemStore() (Store, error) { + return &memStore{ + Book: newKeyBook(), + }, nil +} + +type localStore struct { + sync.Mutex + filename string + flock *flock.Flock +} + +// NewLocalStore constructs a local file backed key.Store +func NewLocalStore(filename string) (Store, error) { + return &localStore{ + filename: filename, + flock: flock.NewFlock(lockPath(filename)), + }, nil +} + +func lockPath(filename string) string { + return fmt.Sprintf("%s.lock", filename) +} + +// PubKey returns the public key for a given ID if it exists +func (s *localStore) PubKey(keyID ID) crypto.PubKey { + s.Lock() + defer s.Unlock() + + kb, err := s.keys() + if err != nil { + return nil + } + return kb.PubKey(keyID) +} + +// PrivKey returns the private key for a given ID if it exists +func (s *localStore) PrivKey(keyID ID) crypto.PrivKey { + s.Lock() + defer s.Unlock() + + kb, err := s.keys() + if err != nil { + return nil + } + return kb.PrivKey(keyID) +} + +// AddPubKey inserts a public key for a given ID +func (s *localStore) AddPubKey(keyID ID, pubKey crypto.PubKey) error { + s.Lock() + defer s.Unlock() + + kb, err := s.keys() + if err != nil { + return err + } + err = kb.AddPubKey(keyID, pubKey) + if err != nil { + return err + } + + return s.saveFile(kb) +} + +// AddPrivKey inserts a private key for a given ID +func (s *localStore) AddPrivKey(keyID ID, privKey crypto.PrivKey) error { + s.Lock() + defer s.Unlock() + + kb, err := s.keys() + if err != nil { + return err + } + err = kb.AddPrivKey(keyID, privKey) + if err != nil { + return err + } + + return s.saveFile(kb) +} + +// IDsWithKeys returns the list of IDs in the KeyBook +func (s *localStore) IDsWithKeys() []ID { + s.Lock() + defer s.Unlock() + + kb, err := s.keys() + if err != nil { + // the keys method will safely return an empty list which we can use bellow + log.Debugf("error loading peers with keys: %q", err.Error()) + return []ID{} + } + return kb.IDsWithKeys() +} + +func (s *localStore) keys() (Book, error) { + log.Debug("reading keys") + + if err := s.flock.Lock(); err != nil { + return nil, err + } + defer func() { + log.Debug("keys read") + s.flock.Unlock() + }() + + kb := newKeyBook() + data, err := ioutil.ReadFile(s.filename) + if err != nil { + if os.IsNotExist(err) { + return kb, nil + } + log.Debug(err.Error()) + return kb, fmt.Errorf("error loading keys: %s", err.Error()) + } + + if err := json.Unmarshal(data, kb); err != nil { + log.Error(err.Error()) + // on bad parsing we simply return an empty keybook + return kb, nil + } + return kb, nil +} + +func (s *localStore) saveFile(kb Book) error { + data, err := json.Marshal(kb) + if err != nil { + log.Debug(err.Error()) + return err + } + + log.Debugf("writing keys: %s", s.filename) + if err := s.flock.Lock(); err != nil { + return err + } + defer func() { + s.flock.Unlock() + log.Debug("keys written") + }() + return ioutil.WriteFile(s.filename, data, 0644) +} diff --git a/key/store_test.go b/key/store_test.go new file mode 100644 index 000000000..580ae6934 --- /dev/null +++ b/key/store_test.go @@ -0,0 +1,50 @@ +package key + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/google/go-cmp/cmp" + cfgtest "github.com/qri-io/qri/config/test" +) + +func TestLocalStore(t *testing.T) { + path := filepath.Join(os.TempDir(), "keys") + if err := os.MkdirAll(path, os.ModePerm); err != nil { + t.Errorf("error creating tmp directory: %s", err.Error()) + } + + ks, err := NewLocalStore(filepath.Join(path, "keystore_test.json")) + if err != nil { + t.Fatal(err) + } + + pi0 := cfgtest.GetTestPeerInfo(0) + k0 := ID("key_id_0") + err = ks.AddPubKey(k0, pi0.PubKey) + if err != nil { + t.Fatal(err) + } + + err = ks.AddPrivKey(k0, pi0.PrivKey) + if err != nil { + t.Fatal(err) + } + + golden := "testdata/keystore.json" + path = filepath.Join(path, "keystore_test.json") + f1, err := ioutil.ReadFile(golden) + if err != nil { + t.Errorf("error reading golden file: %s", err.Error()) + } + f2, err := ioutil.ReadFile(path) + if err != nil { + t.Errorf("error reading written file: %s", err.Error()) + } + + if diff := cmp.Diff(f1, f2); diff != "" { + t.Errorf("result mismatch (-want +got):\n%s", diff) + } +} diff --git a/key/testdata/keystore.json b/key/testdata/keystore.json new file mode 100644 index 000000000..094ced37a --- /dev/null +++ b/key/testdata/keystore.json @@ -0,0 +1 @@ +{"private_keys":{"key_id_0":"CAASqAkwggSkAgEAAoIBAQChp1HiZxTsLQCaHmW3/cc2ZDZpgLwn5o1/nZPgqT7SyXHP5bn7GQMG3kPEQWcl4nhtLX9hkrBEskHrdIlqp9zXFMwBfat+qfzCylGC/QBDF7wT9umLd7nbq7pAxQXteXgntt2Zhg4gE/kEk7vIyL+P9KpWJZ/yjpykgsDC7NPnrr8qZBo2tL0F4w+33nZhEx7Pp7Rnaq22JM8rF+NHCgSkUh63lp7Vhwm9PQoGtt0XTnEKxrMQnUme/IhGNxs84RphxHc5+nW6jYjgm5bcJonGyPU7bq+v51Mr2Ol4RT3L9ZNJgz0SWTSmAtiBLx2ryLrTjmDPSvN7wLm9sWEdWmRVAgMBAAECggEBAJMumrl+jWgz2TZ5sreBEp6NQ5VvpuDVY8PrnzaQIikdTMizK1BaB417VUwdGGM//dG5+R7HxkHl42sT4gH/8GzL/Krm1vwunXplZy3SWSi9NXsf9qgLTGebxasvOCRt0l6mesFLcxT12ma2c+VuEixp4aUqAKWB/1Ex03wm0RFBcSttPHe5ODW8Eaz+ZU8cpObEcZdCIPVxeWqLVdkAImOmsknL0EAxP8Wo/V6Rh5Cg4PnwnfJiQ45C+m6h7NTIw0H4UOncv7EBABra6LqF6Uoda9vmv8CpwaXwR557DPchQglFjtm48jWGeVKO3Zyutizu420eRrFZ0GmJo5flvkkCgYEA0SLysOZNxDgjYA0ihVYL6UbCvYUSADuDyTWREOUiRfmxAmS1xN9o7fieCJnA4aAAnSugtT2BI7HEqT1lLz0YF8NRDKL07TNbkmNLIHXBbXA5saf10N2juhflfIm5/b/W9lC3QsngMR27J25Ztqof6Ur36bIKJ6Y6XvYdlkkZkc8CgYEAxeCHUWMvtHtBID9ZOtrZRNhNJ/uz+2rzVSPd6ZdhEUWsvv/0p7JXmSAp2eoJDDKHeSnVxcxQMqhq0/edUSSzSvDpWha8UU4N8hRpu+M0XZNke0ijhpK6NIqNHPvZdsyFD0VR1Vaj2Ruy+pzih6PhqSnn2ZwvpQJAwBnqc2VCJJsCgYAkQr33hAbpxZ4EkmJw4elwye8L8x2a4rbH1TzQxBm8Lj3Nn26Qsve7gwbLkPULabWRirXzlrVkXfcuLNH1bc9Wl2vfGAYFdokjCYpGF4SxF+s47VlGnJc9tdT5UdvorjF0RaxwrRXtDi2b+Zsee8LKrU/sugzesQif3GZm30fKqwKBgQCQHwHP+HMFfAQqLZma8UzwBK7loUEsrHAAoff+K8CKKPoxvxD9lzqQD8oLqpbeaGsdh6fowe/jhaERM7dEI3vm6GK9t/N/MF+d4tpD+67nPPQhiv13haTTodo3swNnsHx1a+K3hLwf5DnOqLehXW59nET+zPAyudpZUEbft2+eYwKBgCMS6SitXwa2UjFNgkMAaOeJjkjnUKcr1tO/zPtaYPugKgkMQB890q4dcq5rnG2onhJ7hkoMwcrFugbD2nub9AIkaMc6Y46jyh2mSeA0337MpoMp99Jmp2/B1rouYo4IRS25b7jk22yjV8ARCzsxFVQxEwA1Lg8YpaXaifuI+/2O"},"public_keys":{"key_id_0":"CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQChp1HiZxTsLQCaHmW3/cc2ZDZpgLwn5o1/nZPgqT7SyXHP5bn7GQMG3kPEQWcl4nhtLX9hkrBEskHrdIlqp9zXFMwBfat+qfzCylGC/QBDF7wT9umLd7nbq7pAxQXteXgntt2Zhg4gE/kEk7vIyL+P9KpWJZ/yjpykgsDC7NPnrr8qZBo2tL0F4w+33nZhEx7Pp7Rnaq22JM8rF+NHCgSkUh63lp7Vhwm9PQoGtt0XTnEKxrMQnUme/IhGNxs84RphxHc5+nW6jYjgm5bcJonGyPU7bq+v51Mr2Ol4RT3L9ZNJgz0SWTSmAtiBLx2ryLrTjmDPSvN7wLm9sWEdWmRVAgMBAAE="}} \ No newline at end of file diff --git a/profile/id.go b/profile/id.go index 14f2374ba..6303697cb 100644 --- a/profile/id.go +++ b/profile/id.go @@ -15,6 +15,11 @@ func (id ID) String() string { return peer.ID(id).Pretty() } +// Validate exposes the validation interface for ID +func (id ID) Validate() error { + return peer.ID(id).Validate() +} + // MarshalJSON implements the json.Marshaler interface for ID func (id ID) MarshalJSON() ([]byte, error) { return json.Marshal(id.String()) diff --git a/profile/profile.go b/profile/profile.go index 709d32fec..9520fb69e 100644 --- a/profile/profile.go +++ b/profile/profile.go @@ -12,19 +12,26 @@ import ( peer "github.com/libp2p/go-libp2p-core/peer" ma "github.com/multiformats/go-multiaddr" "github.com/qri-io/qri/config" + "github.com/qri-io/qri/key" ) var log = logger.Logger("profile") // Profile defines peer profile details type Profile struct { + // All Profiles are built on public key infrastructure + // PrivKey is the peer's private key, should only be present for the current peer + PrivKey crypto.PrivKey `json:"_,omitempty"` + // PubKey is the peer's public key + PubKey crypto.PubKey `json:"key,omitempty"` + // KeyID is the key identifier used for the keystore + KeyID key.ID `json:"key_id,omitempty"` + ID ID `json:"id"` // Created timestamp Created time.Time `json:"created,omitempty"` // Updated timestamp Updated time.Time `json:"updated,omitempty"` - // PrivKey is the peer's private key, should only be present for the current peer - PrivKey crypto.PrivKey `json:"_,omitempty"` // Peername a handle for the user. min 1 character, max 80. composed of [_,-,a-z,A-Z,1-9] Peername string `json:"peername"` // specifies weather this is a user or an organization @@ -49,6 +56,7 @@ type Profile struct { Twitter string `json:"twitter"` // Online indicates if this peer is currently connected to the network Online bool `json:"online,omitempty"` + // PeerIDs lists any network PeerIDs associated with this profile // in the form /network/peerID PeerIDs []peer.ID `json:"peerIDs"` @@ -109,6 +117,8 @@ func (p *Profile) Decode(sp *config.ProfilePod) error { return fmt.Errorf("invalid private key: %s", err.Error()) } pro.PrivKey = pk + pro.PubKey = pk.GetPublic() + pro.KeyID = pro.GetKeyID() } if sp.Thumb != "" { @@ -176,3 +186,12 @@ func (p *Profile) ValidOwnerProfile() error { // TODO (b5) - confirm PrivKey is valid return nil } + +// GetKeyID returns a KeyID assigned to the profile or falls back +// to the profile ID if none is present +func (p *Profile) GetKeyID() key.ID { + if p.KeyID == key.ID("") { + p.KeyID = key.ID(p.ID.String()) + } + return p.KeyID +} diff --git a/profile/store.go b/profile/store.go index bff96205b..9334b35af 100644 --- a/profile/store.go +++ b/profile/store.go @@ -11,6 +11,7 @@ import ( "github.com/libp2p/go-libp2p-core/peer" "github.com/qri-io/qfs" "github.com/qri-io/qri/config" + "github.com/qri-io/qri/key" "github.com/theckman/go-flock" ) @@ -52,15 +53,20 @@ func NewStore(cfg *config.Config) (Store, error) { return nil, err } + keyStore, err := key.NewStore(cfg) + if err != nil { + return nil, err + } + if cfg.Repo == nil { - return NewMemStore(pro) + return NewMemStore(pro, keyStore) } switch cfg.Repo.Type { case "fs": - return NewLocalStore(filepath.Join(filepath.Dir(cfg.Path()), "peers.json"), pro) + return NewLocalStore(filepath.Join(filepath.Dir(cfg.Path()), "peers.json"), pro, keyStore) case "mem": - return NewMemStore(pro) + return NewMemStore(pro, keyStore) default: return nil, fmt.Errorf("unknown repo type: %s", cfg.Repo.Type) } @@ -69,12 +75,13 @@ func NewStore(cfg *config.Config) (Store, error) { // MemStore is an in-memory implementation of the profile Store interface type MemStore struct { sync.Mutex - owner *Profile - store map[ID]*Profile + owner *Profile + store map[ID]*Profile + keyStore key.Store } // NewMemStore allocates a MemStore -func NewMemStore(owner *Profile) (Store, error) { +func NewMemStore(owner *Profile, ks key.Store) (Store, error) { if err := owner.ValidOwnerProfile(); err != nil { return nil, err } @@ -84,6 +91,7 @@ func NewMemStore(owner *Profile) (Store, error) { store: map[ID]*Profile{ owner.ID: owner, }, + keyStore: ks, }, nil } @@ -99,14 +107,25 @@ func (m *MemStore) SetOwner(own *Profile) error { } // PutProfile adds a peer to this store -func (m *MemStore) PutProfile(profile *Profile) error { - if profile.ID.String() == "" { +func (m *MemStore) PutProfile(p *Profile) error { + if p.ID.String() == "" { return fmt.Errorf("profile.ID is required") } m.Lock() - m.store[profile.ID] = profile + m.store[p.ID] = p m.Unlock() + + if p.PubKey != nil { + if err := m.keyStore.AddPubKey(p.GetKeyID(), p.PubKey); err != nil { + return err + } + } + if p.PrivKey != nil { + if err := m.keyStore.AddPrivKey(p.GetKeyID(), p.PrivKey); err != nil { + return err + } + } return nil } @@ -177,7 +196,13 @@ func (m *MemStore) GetProfile(id ID) (*Profile, error) { if m.store[id] == nil { return nil, ErrNotFound } - return m.store[id], nil + + pro := m.store[id] + pro.KeyID = pro.GetKeyID() + pro.PubKey = m.keyStore.PubKey(pro.GetKeyID()) + pro.PrivKey = m.keyStore.PrivKey(pro.GetKeyID()) + + return pro, nil } // DeleteProfile removes a peer from this store @@ -194,18 +219,20 @@ func (m *MemStore) DeleteProfile(id ID) error { type LocalStore struct { sync.Mutex owner *Profile + keyStore key.Store filename string flock *flock.Flock } // NewLocalStore allocates a LocalStore -func NewLocalStore(filename string, owner *Profile) (Store, error) { +func NewLocalStore(filename string, owner *Profile, ks key.Store) (Store, error) { if err := owner.ValidOwnerProfile(); err != nil { return nil, err } return &LocalStore{ owner: owner, + keyStore: ks, filename: filename, flock: flock.NewFlock(lockPath(filename)), }, nil @@ -223,7 +250,7 @@ func (r *LocalStore) Owner() *Profile { // SetOwner updates the owner profile func (r *LocalStore) SetOwner(own *Profile) error { r.owner = own - return nil + return r.PutProfile(own) } // PutProfile adds a peer to the store @@ -241,6 +268,17 @@ func (r *LocalStore) PutProfile(p *Profile) error { // explicitly remove Online flag enc.Online = false + if p.PubKey != nil { + if err := r.keyStore.AddPubKey(p.GetKeyID(), p.PubKey); err != nil { + return err + } + } + if p.PrivKey != nil { + if err := r.keyStore.AddPrivKey(p.GetKeyID(), p.PrivKey); err != nil { + return err + } + } + r.Lock() defer r.Unlock() @@ -337,6 +375,9 @@ func (r *LocalStore) GetProfile(id ID) (*Profile, error) { if ids == proid { pro := &Profile{} err := pro.Decode(p) + pro.KeyID = pro.GetKeyID() + pro.PubKey = r.keyStore.PubKey(pro.GetKeyID()) + pro.PrivKey = r.keyStore.PrivKey(pro.GetKeyID()) return pro, err } } diff --git a/profile/store_test.go b/profile/store_test.go index d0589afbb..d84a52d0f 100644 --- a/profile/store_test.go +++ b/profile/store_test.go @@ -11,6 +11,7 @@ import ( "github.com/libp2p/go-libp2p-core/peer" "github.com/qri-io/qri/config" cfgtest "github.com/qri-io/qri/config/test" + "github.com/qri-io/qri/key" ) func TestPutProfileWithAddresses(t *testing.T) { @@ -35,7 +36,13 @@ func TestPutProfileWithAddresses(t *testing.T) { } pi0 := cfgtest.GetTestPeerInfo(0) - ps, err := NewLocalStore(filepath.Join(path, "profiles.json"), &Profile{PrivKey: pi0.PrivKey, Peername: "user"}) + + ks, err := key.NewMemStore() + if err != nil { + t.Fatal(err) + } + + ps, err := NewLocalStore(filepath.Join(path, "profiles.json"), &Profile{PrivKey: pi0.PrivKey, Peername: "user"}, ks) if err != nil { t.Fatal(err) } @@ -60,3 +67,54 @@ func TestPutProfileWithAddresses(t *testing.T) { t.Errorf("result mismatch (-want +got):\n%s", diff) } } + +func TestProfilesWithKeys(t *testing.T) { + pi0 := cfgtest.GetTestPeerInfo(0) + + ks, err := key.NewMemStore() + if err != nil { + t.Fatal(err) + } + + path := filepath.Join(os.TempDir(), "profile_keys") + if err := os.MkdirAll(path, os.ModePerm); err != nil { + t.Errorf("error creating tmp directory: %s", err.Error()) + } + + ps, err := NewLocalStore(filepath.Join(path, "profiles.json"), &Profile{PrivKey: pi0.PrivKey, Peername: "user"}, ks) + if err != nil { + t.Fatal(err) + } + + pp := &config.ProfilePod{ + ID: pi0.PeerID.String(), + Peername: "p0", + Created: time.Unix(1234567890, 0).In(time.UTC), + Updated: time.Unix(1234567890, 0).In(time.UTC), + } + pro, err := NewProfile(pp) + if err != nil { + t.Fatal(err) + } + + pro.PrivKey = pi0.PrivKey + pro.PubKey = pi0.PubKey + + err = ps.PutProfile(pro) + if err != nil { + t.Fatal(err) + } + + tPro, err := ps.GetProfile(pro.ID) + if err != nil { + t.Fatal(err) + } + + if tPro.PrivKey != pi0.PrivKey { + t.Fatalf("keys don't match") + } + + if tPro.PubKey != pi0.PubKey { + t.Fatalf("keys don't match") + } +} diff --git a/repo/fs/fs_test.go b/repo/fs/fs_test.go index 9acf62269..c8e323253 100644 --- a/repo/fs/fs_test.go +++ b/repo/fs/fs_test.go @@ -13,6 +13,7 @@ import ( "github.com/qri-io/qri/dsref" dsrefspec "github.com/qri-io/qri/dsref/spec" "github.com/qri-io/qri/event" + "github.com/qri-io/qri/key" "github.com/qri-io/qri/logbook" "github.com/qri-io/qri/logbook/oplog" "github.com/qri-io/qri/profile" @@ -47,7 +48,12 @@ func TestRepo(t *testing.T) { t.Fatal(err) } - pros, err := profile.NewMemStore(pro) + keyStore, err := key.NewMemStore() + if err != nil { + t.Fatal(err) + } + + pros, err := profile.NewMemStore(pro, keyStore) if err != nil { t.Fatal(err) } @@ -102,7 +108,12 @@ func TestResolveRef(t *testing.T) { t.Fatal(err) } - pros, err := profile.NewMemStore(pro) + keyStore, err := key.NewMemStore() + if err != nil { + t.Fatal(err) + } + + pros, err := profile.NewMemStore(pro, keyStore) if err != nil { t.Fatal(err) } diff --git a/repo/mem_repo.go b/repo/mem_repo.go index db43ac67b..7df9fecc5 100644 --- a/repo/mem_repo.go +++ b/repo/mem_repo.go @@ -6,11 +6,13 @@ import ( "sync" crypto "github.com/libp2p/go-libp2p-core/crypto" + "github.com/qri-io/qfs" "github.com/qri-io/qfs/muxfs" "github.com/qri-io/qri/dscache" "github.com/qri-io/qri/dsref" "github.com/qri-io/qri/event" + "github.com/qri-io/qri/key" "github.com/qri-io/qri/logbook" "github.com/qri-io/qri/profile" ) @@ -37,7 +39,11 @@ var _ Repo = (*MemRepo)(nil) // NewMemRepoWithProfile creates a new in-memory repository and an empty profile // store owned by the given profile func NewMemRepoWithProfile(ctx context.Context, owner *profile.Profile, fs *muxfs.Mux, bus event.Bus) (*MemRepo, error) { - pros, err := profile.NewMemStore(owner) + keyStore, err := key.NewMemStore() + if err != nil { + return nil, err + } + pros, err := profile.NewMemStore(owner, keyStore) if err != nil { return nil, err }