github.com/qri-io/qri@v0.10.1-0.20220104210721-c771715036cb/auth/key/store.go (about)

     1  package key
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"os"
     9  	"path/filepath"
    10  	"sync"
    11  
    12  	"github.com/gofrs/flock"
    13  	"github.com/libp2p/go-libp2p-core/crypto"
    14  	"github.com/qri-io/qri/config"
    15  )
    16  
    17  // ErrKeyAndIDMismatch occurs when a key identifier doesn't match it's public
    18  // key
    19  var ErrKeyAndIDMismatch = fmt.Errorf("public key does not match identifier")
    20  
    21  // Store is an abstraction over a KeyBook
    22  // In the future we may expand this interface to store symmetric encryption keys
    23  type Store interface {
    24  	Book
    25  }
    26  
    27  // NewStore constructs a keys.Store backed by memory or local file
    28  func NewStore(cfg *config.Config) (Store, error) {
    29  	if cfg.Repo == nil {
    30  		return NewMemStore()
    31  	}
    32  
    33  	switch cfg.Repo.Type {
    34  	case "fs":
    35  		// Don't create a localstore with the empty path, this will use the current directory
    36  		if cfg.Path() == "" {
    37  			return nil, fmt.Errorf("new key.LocalStore requires non-empty path")
    38  		}
    39  		return NewLocalStore(filepath.Join(filepath.Dir(cfg.Path()), "keystore.json"))
    40  	case "mem":
    41  		return NewMemStore()
    42  	default:
    43  		return nil, fmt.Errorf("unknown repo type: %s", cfg.Repo.Type)
    44  	}
    45  }
    46  
    47  type memStore struct {
    48  	Book
    49  }
    50  
    51  // NewMemStore constructs an in-memory key.Store
    52  func NewMemStore() (Store, error) {
    53  	return &memStore{
    54  		Book: newKeyBook(),
    55  	}, nil
    56  }
    57  
    58  type localStore struct {
    59  	sync.Mutex
    60  	filename string
    61  	flock    *flock.Flock
    62  }
    63  
    64  // NewLocalStore constructs a local file backed key.Store
    65  func NewLocalStore(filename string) (Store, error) {
    66  	return &localStore{
    67  		filename: filename,
    68  		flock:    flock.New(lockPath(filename)),
    69  	}, nil
    70  }
    71  
    72  func lockPath(filename string) string {
    73  	return fmt.Sprintf("%s.lock", filename)
    74  }
    75  
    76  // PubKey returns the public key for a given ID if it exists
    77  func (s *localStore) PubKey(ctx context.Context, keyID ID) crypto.PubKey {
    78  	s.Lock()
    79  	defer s.Unlock()
    80  
    81  	kb, err := s.keys()
    82  	if err != nil {
    83  		return nil
    84  	}
    85  	return kb.PubKey(ctx, keyID)
    86  }
    87  
    88  // PrivKey returns the private key for a given ID if it exists
    89  func (s *localStore) PrivKey(ctx context.Context, keyID ID) crypto.PrivKey {
    90  	s.Lock()
    91  	defer s.Unlock()
    92  
    93  	kb, err := s.keys()
    94  	if err != nil {
    95  		return nil
    96  	}
    97  	return kb.PrivKey(ctx, keyID)
    98  }
    99  
   100  // AddPubKey inserts a public key for a given ID
   101  func (s *localStore) AddPubKey(ctx context.Context, keyID ID, pubKey crypto.PubKey) error {
   102  	s.Lock()
   103  	defer s.Unlock()
   104  
   105  	kb, err := s.keys()
   106  	if err != nil {
   107  		return err
   108  	}
   109  	if !keyID.MatchesPublicKey(pubKey) {
   110  		return fmt.Errorf("%w id: %q", ErrKeyAndIDMismatch, keyID.Pretty())
   111  	}
   112  	err = kb.AddPubKey(ctx, keyID, pubKey)
   113  	if err != nil {
   114  		return err
   115  	}
   116  
   117  	return s.saveFile(kb)
   118  }
   119  
   120  // AddPrivKey inserts a private key for a given ID
   121  func (s *localStore) AddPrivKey(ctx context.Context, keyID ID, privKey crypto.PrivKey) error {
   122  	s.Lock()
   123  	defer s.Unlock()
   124  
   125  	if !keyID.MatchesPrivateKey(privKey) {
   126  		return fmt.Errorf("%w id: %q", ErrKeyAndIDMismatch, keyID.Pretty())
   127  	}
   128  
   129  	kb, err := s.keys()
   130  	if err != nil {
   131  		return err
   132  	}
   133  	err = kb.AddPrivKey(ctx, keyID, privKey)
   134  	if err != nil {
   135  		return err
   136  	}
   137  
   138  	return s.saveFile(kb)
   139  }
   140  
   141  // IDsWithKeys returns the list of IDs in the KeyBook
   142  func (s *localStore) IDsWithKeys(ctx context.Context) []ID {
   143  	s.Lock()
   144  	defer s.Unlock()
   145  
   146  	kb, err := s.keys()
   147  	if err != nil {
   148  		// the keys method will safely return an empty list which we can use bellow
   149  		log.Debugf("error loading peers with keys: %q", err.Error())
   150  		return []ID{}
   151  	}
   152  	return kb.IDsWithKeys(ctx)
   153  }
   154  
   155  func (s *localStore) keys() (Book, error) {
   156  	log.Debug("reading keys")
   157  
   158  	if err := s.flock.Lock(); err != nil {
   159  		return nil, err
   160  	}
   161  	defer func() {
   162  		log.Debug("keys read")
   163  		s.flock.Unlock()
   164  	}()
   165  
   166  	kb := newKeyBook()
   167  	data, err := ioutil.ReadFile(s.filename)
   168  	if err != nil {
   169  		if os.IsNotExist(err) {
   170  			return kb, nil
   171  		}
   172  		log.Debug(err.Error())
   173  		return kb, fmt.Errorf("error loading keys: %s", err.Error())
   174  	}
   175  
   176  	if err := json.Unmarshal(data, kb); err != nil {
   177  		log.Error(err.Error())
   178  		// on bad parsing we simply return an empty keybook
   179  		return kb, nil
   180  	}
   181  	return kb, nil
   182  }
   183  
   184  func (s *localStore) saveFile(kb Book) error {
   185  	data, err := json.Marshal(kb)
   186  	if err != nil {
   187  		log.Debug(err.Error())
   188  		return err
   189  	}
   190  
   191  	log.Debugf("writing keys: %s", s.filename)
   192  	if err := s.flock.Lock(); err != nil {
   193  		return err
   194  	}
   195  	defer func() {
   196  		s.flock.Unlock()
   197  		log.Debug("keys written")
   198  	}()
   199  	return ioutil.WriteFile(s.filename, data, 0644)
   200  }