|
|
@ -6,6 +6,7 @@ like standard ssh key storage. |
|
|
|
package filestorage |
|
|
|
|
|
|
|
import ( |
|
|
|
"encoding/hex" |
|
|
|
"fmt" |
|
|
|
"io/ioutil" |
|
|
|
"os" |
|
|
@ -13,19 +14,25 @@ import ( |
|
|
|
"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 = "tlc" |
|
|
|
PubExt = "pub" |
|
|
|
keyPerm = os.FileMode(0600) |
|
|
|
pubPerm = os.FileMode(0644) |
|
|
|
dirPerm = os.FileMode(0700) |
|
|
|
// 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 |
|
|
|
} |
|
|
@ -36,9 +43,11 @@ type FileStore struct { |
|
|
|
// 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} |
|
|
|
} |
|
|
|
|
|
|
@ -49,7 +58,7 @@ func (s FileStore) assertStorage() keys.Storage { |
|
|
|
|
|
|
|
// 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, key []byte, info keys.Info) error { |
|
|
|
func (s FileStore) Put(name string, salt, key []byte, info keys.Info) error { |
|
|
|
pub, priv := s.nameToPaths(name) |
|
|
|
|
|
|
|
// write public info
|
|
|
@ -59,31 +68,34 @@ func (s FileStore) Put(name string, key []byte, info keys.Info) error { |
|
|
|
} |
|
|
|
|
|
|
|
// write private info
|
|
|
|
return write(priv, name, key) |
|
|
|
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) ([]byte, keys.Info, error) { |
|
|
|
func (s FileStore) Get(name string) (salt []byte, key []byte, info keys.Info, err error) { |
|
|
|
pub, priv := s.nameToPaths(name) |
|
|
|
|
|
|
|
info, err := readInfo(pub) |
|
|
|
info, err = readInfo(pub) |
|
|
|
if err != nil { |
|
|
|
return nil, info, err |
|
|
|
return nil, nil, info, err |
|
|
|
} |
|
|
|
|
|
|
|
key, _, err := read(priv) |
|
|
|
return key, info.Format(), 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) |
|
|
|
defer dir.Close() |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
return nil, errors.Wrap(err, "List Keys") |
|
|
|
} |
|
|
|
|
|
|
|
names, err := dir.Readdirnames(0) |
|
|
|
if err != nil { |
|
|
|
return nil, errors.Wrap(err, "List Keys") |
|
|
@ -111,61 +123,121 @@ func (s FileStore) List() (keys.Infos, error) { |
|
|
|
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 writeInfo(path string, info keys.Info) error { |
|
|
|
return write(path, info.Name, info.PubKey.Bytes()) |
|
|
|
return path.Join(s.keyDir, pubName), path.Join(s.keyDir, privName) |
|
|
|
} |
|
|
|
|
|
|
|
func readInfo(path string) (info keys.Info, err error) { |
|
|
|
var data []byte |
|
|
|
data, info.Name, err = read(path) |
|
|
|
f, err := os.Open(path) |
|
|
|
defer f.Close() |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
return |
|
|
|
return info, errors.Wrap(err, "Reading data") |
|
|
|
} |
|
|
|
pk, err := crypto.PubKeyFromBytes(data) |
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
return info, nil |
|
|
|
} |
|
|
|
|
|
|
|
func read(path string) ([]byte, string, error) { |
|
|
|
func read(path string) (salt, key []byte, name string, err error) { |
|
|
|
f, err := os.Open(path) |
|
|
|
defer f.Close() |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
return nil, "", errors.Wrap(err, "Reading data") |
|
|
|
return nil, nil, "", errors.Wrap(err, "Reading data") |
|
|
|
} |
|
|
|
|
|
|
|
d, err := ioutil.ReadAll(f) |
|
|
|
if err != nil { |
|
|
|
return nil, "", errors.Wrap(err, "Reading data") |
|
|
|
return nil, nil, "", errors.Wrap(err, "Reading data") |
|
|
|
} |
|
|
|
|
|
|
|
block, headers, key, err := crypto.DecodeArmor(string(d)) |
|
|
|
if err != nil { |
|
|
|
return nil, "", errors.Wrap(err, "Invalid Armor") |
|
|
|
return nil, nil, "", errors.Wrap(err, "Invalid Armor") |
|
|
|
} |
|
|
|
|
|
|
|
if block != BlockType { |
|
|
|
return nil, "", errors.Errorf("Unknown key type: %s", block) |
|
|
|
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") |
|
|
|
} |
|
|
|
return key, headers["name"], nil |
|
|
|
|
|
|
|
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 write(path, name string, key []byte) error { |
|
|
|
func writeInfo(path string, info keys.Info) error { |
|
|
|
f, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, keyPerm) |
|
|
|
defer f.Close() |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
return errors.Wrap(err, "Writing data") |
|
|
|
} |
|
|
|
|
|
|
|
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) |
|
|
|
defer f.Close() |
|
|
|
headers := map[string]string{"name": name} |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
return errors.Wrap(err, "Writing data") |
|
|
|
} |
|
|
|
|
|
|
|
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") |
|
|
|
} |