- /*
- package filestorage provides a secure on-disk storage of private keys and
- metadata. Security is enforced by file and directory permissions, much
- like standard ssh key storage.
- */
- package filestorage
-
- import (
- "encoding/hex"
- "fmt"
- "io/ioutil"
- "os"
- "path"
- "strings"
-
- "github.com/pkg/errors"
-
- crypto "github.com/tendermint/go-crypto"
- keys "github.com/tendermint/go-crypto/keys"
- )
-
- const (
- // BlockType is the type of block.
- BlockType = "Tendermint Light Client"
-
- // PrivExt is the extension for private keys.
- PrivExt = "tlc"
- // PubExt is the extensions for public keys.
- PubExt = "pub"
-
- keyPerm = os.FileMode(0600)
- // pubPerm = os.FileMode(0644)
- dirPerm = os.FileMode(0700)
- )
-
- // FileStore is a file-based key storage with tight permissions.
- type FileStore struct {
- keyDir string
- }
-
- // New creates an instance of file-based key storage with tight permissions
- //
- // dir should be an absolute path of a directory owner by this user. It will
- // be created if it doesn't exist already.
- func New(dir string) FileStore {
- err := os.MkdirAll(dir, dirPerm)
-
- if err != nil {
- panic(err)
- }
-
- return FileStore{dir}
- }
-
- // assert FileStore satisfies keys.Storage
- var _ keys.Storage = FileStore{}
-
- // Put creates two files, one with the public info as json, the other
- // with the (encoded) private key as gpg ascii-armor style
- func (s FileStore) Put(name string, salt, key []byte, info keys.Info) error {
- pub, priv := s.nameToPaths(name)
-
- // write public info
- err := writeInfo(pub, info)
- if err != nil {
- return err
- }
-
- // write private info
- return write(priv, name, salt, key)
- }
-
- // Get loads the info and (encoded) private key from the directory
- // It uses `name` to generate the filename, and returns an error if the
- // files don't exist or are in the incorrect format
- func (s FileStore) Get(name string) (salt []byte, key []byte, info keys.Info, err error) {
- pub, priv := s.nameToPaths(name)
-
- info, err = readInfo(pub)
- if err != nil {
- return nil, nil, info, err
- }
-
- salt, key, _, err = read(priv)
- return salt, key, info.Format(), err
- }
-
- // List parses the key directory for public info and returns a list of
- // Info for all keys located in this directory.
- func (s FileStore) List() (keys.Infos, error) {
- dir, err := os.Open(s.keyDir)
- if err != nil {
- return nil, errors.Wrap(err, "List Keys")
- }
- defer dir.Close()
-
- names, err := dir.Readdirnames(0)
- if err != nil {
- return nil, errors.Wrap(err, "List Keys")
- }
-
- // filter names for .pub ending and load them one by one
- // half the files is a good guess for pre-allocating the slice
- infos := make([]keys.Info, 0, len(names)/2)
- for _, name := range names {
- if strings.HasSuffix(name, PubExt) {
- p := path.Join(s.keyDir, name)
- info, err := readInfo(p)
- if err != nil {
- return nil, err
- }
- infos = append(infos, info.Format())
- }
- }
-
- return infos, nil
- }
-
- // Delete permanently removes the public and private info for the named key
- // The calling function should provide some security checks first.
- func (s FileStore) Delete(name string) error {
- pub, priv := s.nameToPaths(name)
- err := os.Remove(priv)
-
- if err != nil {
- return errors.Wrap(err, "Deleting Private Key")
- }
-
- err = os.Remove(pub)
-
- return errors.Wrap(err, "Deleting Public Key")
- }
-
- func (s FileStore) nameToPaths(name string) (pub, priv string) {
- privName := fmt.Sprintf("%s.%s", name, PrivExt)
- pubName := fmt.Sprintf("%s.%s", name, PubExt)
-
- return path.Join(s.keyDir, pubName), path.Join(s.keyDir, privName)
- }
-
- func readInfo(path string) (info keys.Info, err error) {
- f, err := os.Open(path)
- if err != nil {
- return info, errors.Wrap(err, "Reading data")
- }
- defer f.Close()
-
- d, err := ioutil.ReadAll(f)
- if err != nil {
- return info, errors.Wrap(err, "Reading data")
- }
-
- block, headers, key, err := crypto.DecodeArmor(string(d))
- if err != nil {
- return info, errors.Wrap(err, "Invalid Armor")
- }
-
- if block != BlockType {
- return info, errors.Errorf("Unknown key type: %s", block)
- }
-
- pk, _ := crypto.PubKeyFromBytes(key)
- info.Name = headers["name"]
- info.PubKey = pk
-
- return info, nil
- }
-
- func read(path string) (salt, key []byte, name string, err error) {
- f, err := os.Open(path)
- if err != nil {
- return nil, nil, "", errors.Wrap(err, "Reading data")
- }
- defer f.Close()
-
- d, err := ioutil.ReadAll(f)
- if err != nil {
- return nil, nil, "", errors.Wrap(err, "Reading data")
- }
-
- block, headers, key, err := crypto.DecodeArmor(string(d))
- if err != nil {
- return nil, nil, "", errors.Wrap(err, "Invalid Armor")
- }
-
- if block != BlockType {
- return nil, nil, "", errors.Errorf("Unknown key type: %s", block)
- }
-
- if headers["kdf"] != "bcrypt" {
- return nil, nil, "", errors.Errorf("Unrecognized KDF type: %v", headers["kdf"])
- }
-
- if headers["salt"] == "" {
- return nil, nil, "", errors.Errorf("Missing salt bytes")
- }
-
- salt, err = hex.DecodeString(headers["salt"])
- if err != nil {
- return nil, nil, "", errors.Errorf("Error decoding salt: %v", err.Error())
- }
-
- return salt, key, headers["name"], nil
- }
-
- func writeInfo(path string, info keys.Info) error {
- f, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, keyPerm)
- if err != nil {
- return errors.Wrap(err, "Writing data")
- }
- defer f.Close()
-
- headers := map[string]string{"name": info.Name}
- text := crypto.EncodeArmor(BlockType, headers, info.PubKey.Bytes())
- _, err = f.WriteString(text)
-
- return errors.Wrap(err, "Writing data")
- }
-
- func write(path, name string, salt, key []byte) error {
- f, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, keyPerm)
- if err != nil {
- return errors.Wrap(err, "Writing data")
- }
- defer f.Close()
-
- headers := map[string]string{
- "name": name,
- "kdf": "bcrypt",
- "salt": fmt.Sprintf("%X", salt),
- }
-
- text := crypto.EncodeArmor(BlockType, headers, key)
- _, err = f.WriteString(text)
-
- return errors.Wrap(err, "Writing data")
- }
|