/*
|
|
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")
|
|
}
|