mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-08-20 10:52:25 +00:00
signer/storage abstract aes-gcm encryption into a layer above all storages
This commit is contained in:
parent
9b649bcc09
commit
17c81b5e64
7 changed files with 240 additions and 383 deletions
|
|
@ -17,219 +17,26 @@
|
||||||
package storage
|
package storage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"github.com/ethereum/go-ethereum/cmd/clef/dbutil"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
|
|
||||||
// here we are adding multiple default supported db drivers
|
|
||||||
_ "github.com/go-sql-driver/mysql"
|
|
||||||
_ "github.com/lib/pq"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// DBStorage is a storage type which is backed by a general purpose database
|
// DBStorageAPI is a storage api which is backed by a general purpose database
|
||||||
type DBStorage struct {
|
type DBStorageAPI struct {
|
||||||
driverName string
|
kvstore *dbutil.KVStore
|
||||||
dataSourceName string
|
|
||||||
tableName string
|
|
||||||
db *sql.DB
|
|
||||||
key []byte
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DBRow is the structure to hold a row of our configuration database
|
// Get returns the previously stored value, or an error if the key is 0-length
|
||||||
// table schemas for all three tables (kps, js, config) are the same
|
// or unknown.
|
||||||
type DBRow struct {
|
func (api *DBStorageAPI) Get(key string) (string, error) {
|
||||||
id int
|
return api.kvstore.Get(key)
|
||||||
key string
|
|
||||||
val string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default table name for storages
|
|
||||||
const (
|
|
||||||
PasswordTable = "kps"
|
|
||||||
ConfigTable = "config"
|
|
||||||
JsTable = "js"
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewDBStorage create new database backed storage
|
|
||||||
func NewDBStorage(key []byte, driverName, dataSourceName, tableName string) (*DBStorage, error) {
|
|
||||||
// sql.Open only validates the input, but didn't create a connection
|
|
||||||
db, err := sql.Open(driverName, dataSourceName)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("failed to validate driver: #{driverName}, #{dataSourceName}")
|
|
||||||
db.Close()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connects to the database and make sure it is ok, connection will be closed shortly since default MaxIdle is 0
|
|
||||||
err = db.Ping()
|
|
||||||
if err != nil {
|
|
||||||
log.Error("failed to connect to database: #{dataSourceName}")
|
|
||||||
db.Close()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// set connection limits
|
|
||||||
db.SetMaxOpenConns(5)
|
|
||||||
|
|
||||||
// init table
|
|
||||||
initTable(driverName, tableName, db)
|
|
||||||
|
|
||||||
return &DBStorage{
|
|
||||||
driverName: driverName,
|
|
||||||
dataSourceName: dataSourceName,
|
|
||||||
tableName: tableName,
|
|
||||||
db: db,
|
|
||||||
key: key,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func initTable(driverName, tableName string, db *sql.DB) error {
|
|
||||||
var err error
|
|
||||||
switch driverName {
|
|
||||||
case "postgres":
|
|
||||||
_, err = db.Exec(fmt.Sprintf(`
|
|
||||||
CREATE TABLE IF NOT EXISTS %s (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
k VARCHAR(255) UNIQUE NOT NULL,
|
|
||||||
v TEXT NOT NULL
|
|
||||||
)
|
|
||||||
`, tableName))
|
|
||||||
case "mysql":
|
|
||||||
_, err = db.Exec(fmt.Sprintf(`
|
|
||||||
CREATE TABLE IF NOT EXISTS %s (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
k VARCHAR(255) UNIQUE NOT NULL,
|
|
||||||
v TEXT NOT NULL
|
|
||||||
)
|
|
||||||
`, tableName))
|
|
||||||
case "sqlite3":
|
|
||||||
_, err = db.Exec(fmt.Sprintf(`
|
|
||||||
CREATE TABLE IF NOT EXISTS %s (
|
|
||||||
id INTEGER PRIMARY KEY,
|
|
||||||
k TEXT,
|
|
||||||
v TEXT
|
|
||||||
)
|
|
||||||
`, tableName))
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Put stores a value by key. 0-length keys results in noop.
|
// Put stores a value by key. 0-length keys results in noop.
|
||||||
func (s *DBStorage) Put(key, value string) {
|
func (api *DBStorageAPI) Put(key, value string) {
|
||||||
if len(key) == 0 {
|
api.kvstore.Put(key, value)
|
||||||
return
|
|
||||||
}
|
|
||||||
ciphertext, iv, err := Encrypt(s.key, []byte(value), []byte(key))
|
|
||||||
if err != nil {
|
|
||||||
log.Warn("Failed to encrypt entry", "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
creds := StoredCredential{Iv: iv, CipherText: ciphertext}
|
|
||||||
sql := s.formatSQL(getSQL)
|
|
||||||
_, exist, err := s.queryRow(sql, key)
|
|
||||||
if err != nil {
|
|
||||||
log.Warn("Failed to execute SQL", "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
raw, err := json.Marshal(creds)
|
|
||||||
if err != nil {
|
|
||||||
log.Warn("Failed to marshal StoredCredential data")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !exist {
|
|
||||||
sql = s.formatSQL(insertSQL)
|
|
||||||
s.exec(sql, key, raw)
|
|
||||||
} else {
|
|
||||||
sql = s.formatSQL(updateSQL)
|
|
||||||
s.exec(sql, raw, key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get returns the previously stored value, or an error if it does not exist or
|
|
||||||
// key is of 0-length.
|
|
||||||
func (s *DBStorage) Get(key string) (string, error) {
|
|
||||||
sql := s.formatSQL(getSQL)
|
|
||||||
row, exist, err := s.queryRow(sql, key)
|
|
||||||
if err != nil {
|
|
||||||
log.Warn("Failed to execute SQL", "err", err)
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if !exist {
|
|
||||||
log.Warn("Key does not exist", "key", key)
|
|
||||||
return "", ErrNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
cred := StoredCredential{}
|
|
||||||
if err = json.Unmarshal([]byte(row.val), &cred); err != nil {
|
|
||||||
log.Warn("Failed to unmarshall stored json", "err", err)
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
entry, err := Decrypt(s.key, cred.Iv, cred.CipherText, []byte(key))
|
|
||||||
if err != nil {
|
|
||||||
log.Warn("Failed to decrypt key", "key", key)
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return string(entry), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Del removes a key-value pair. If the key doesn't exist, the method is a noop.
|
// Del removes a key-value pair. If the key doesn't exist, the method is a noop.
|
||||||
func (s *DBStorage) Del(key string) {
|
func (api *DBStorageAPI) Del(key string) {
|
||||||
sql := s.formatSQL(deleteSQL)
|
api.kvstore.Del(key)
|
||||||
s.exec(sql, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *DBStorage) exec(query string, args ...interface{}) {
|
|
||||||
_, err := s.db.Exec(query, args...)
|
|
||||||
if err != nil {
|
|
||||||
log.Warn("Failed to execute sql", query, args)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *DBStorage) queryRow(query string, args ...interface{}) (*DBRow, bool, error) {
|
|
||||||
row := DBRow{}
|
|
||||||
err := s.db.QueryRow(query, args...).Scan(&row.id, &row.key, &row.val)
|
|
||||||
if err != nil && err != sql.ErrNoRows {
|
|
||||||
return nil, false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if row.id == 0 {
|
|
||||||
return nil, false, nil
|
|
||||||
}
|
|
||||||
return &row, true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
getSQL string = "SELECT * FROM tableName WHERE k = ?"
|
|
||||||
updateSQL string = "UPDATE tableName SET v = ? WHERE k = ?"
|
|
||||||
insertSQL string = "INSERT INTO tableName (k, v) VALUES (?, ?)"
|
|
||||||
deleteSQL string = "DELETE FROM tableName WHERE k = ?"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s *DBStorage) formatSQL(sql string) string {
|
|
||||||
switch s.driverName {
|
|
||||||
case "postgres":
|
|
||||||
params := strings.Count(sql, "?")
|
|
||||||
for i := 1; i <= params; i++ {
|
|
||||||
sql = strings.Replace(sql, "?", fmt.Sprintf("$%d", i), 1)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
// for MS SQL Server / MySQL / SQLite
|
|
||||||
// since they're already using ? as placeholder, do nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.ReplaceAll(sql, "tableName", s.tableName)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close sql.DB
|
|
||||||
func (s *DBStorage) Close() {
|
|
||||||
s.db.Close()
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,69 +0,0 @@
|
||||||
package storage
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io/ioutil"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
_ "github.com/mattn/go-sqlite3"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ds *DBStorage
|
|
||||||
key string
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
key = "AES256Key-32Characters1234567890"
|
|
||||||
tmpDir, _ := ioutil.TempDir("", "eth-encrypted-db-storge-test")
|
|
||||||
ds, _ = NewDBStorage([]byte(key), "sqlite3", filepath.Join(tmpDir, "test.db"), "kps")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDBStorage(t *testing.T) {
|
|
||||||
// test Put
|
|
||||||
k1, v1 := "k1", "v1"
|
|
||||||
ds.Put(k1, v1)
|
|
||||||
|
|
||||||
// test Get
|
|
||||||
ret, err := ds.Get(k1)
|
|
||||||
if err != nil || ret != v1 {
|
|
||||||
t.Fatal("Get didn't return correct result")
|
|
||||||
}
|
|
||||||
|
|
||||||
// test Put when there's duplicate
|
|
||||||
v2 := "v2"
|
|
||||||
ds.Put(k1, v2)
|
|
||||||
ret, err = ds.Get(k1)
|
|
||||||
if err != nil || ret != v2 {
|
|
||||||
t.Fatal("Get didn't return correct result")
|
|
||||||
}
|
|
||||||
|
|
||||||
// test Del
|
|
||||||
ds.Del(k1)
|
|
||||||
ret, err = ds.Get(k1)
|
|
||||||
if err != ErrNotFound {
|
|
||||||
t.Fatal("Del didn't work as expected")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSwappedKeysForDBStorage(t *testing.T) {
|
|
||||||
ds.Put("k1", "v1")
|
|
||||||
ds.Put("k2", "v2")
|
|
||||||
|
|
||||||
// now make a modified copy
|
|
||||||
swap := func() {
|
|
||||||
creds1, _, _ := ds.queryRow("SELECT * FROM kps WHERE k = 'k1'")
|
|
||||||
creds2, _, _ := ds.queryRow("SELECT * FROM kps WHERE k = 'k2'")
|
|
||||||
ds.exec("UPDATE kps SET v = ? WHERE k = ?", creds1.val, "k2")
|
|
||||||
ds.exec("UPDATE kps SET v = ? WHERE k = ?", creds2.val, "k1")
|
|
||||||
}
|
|
||||||
swap()
|
|
||||||
if v, _ := ds.Get("k1"); v != "" {
|
|
||||||
t.Errorf("swapped value should return empty")
|
|
||||||
}
|
|
||||||
swap()
|
|
||||||
if v, _ := ds.Get("k1"); v != "v1" {
|
|
||||||
t.Errorf(v)
|
|
||||||
t.Errorf("double-swapped value should work fine")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
32
signer/storage/ephemeral_storage.go
Normal file
32
signer/storage/ephemeral_storage.go
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
package storage
|
||||||
|
|
||||||
|
// EphemeralStorageAPI is an in-memory storage api that does
|
||||||
|
// not persist values to disk. Mainly used for testing
|
||||||
|
type EphemeralStorageAPI struct {
|
||||||
|
data map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put stores a value by key. 0-length keys results in noop.
|
||||||
|
func (s *EphemeralStorageAPI) Put(key, value string) {
|
||||||
|
if len(key) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.data[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns the previously stored value, or an error if the key is 0-length
|
||||||
|
// or unknown.
|
||||||
|
func (s *EphemeralStorageAPI) Get(key string) (string, error) {
|
||||||
|
if len(key) == 0 {
|
||||||
|
return "", ErrZeroKey
|
||||||
|
}
|
||||||
|
if v, ok := s.data[key]; ok {
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
return "", ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// Del removes a key-value pair. If the key doesn't exist, the method is a noop.
|
||||||
|
func (s *EphemeralStorageAPI) Del(key string) {
|
||||||
|
delete(s.data, key)
|
||||||
|
}
|
||||||
|
|
@ -24,85 +24,82 @@ import (
|
||||||
"github.com/ethereum/go-ethereum/log"
|
"github.com/ethereum/go-ethereum/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AESEncryptedStorage is a storage type which is backed by a json-file. The json-file contains
|
// FileStorageAPI is a storage type which is backed by a json-file.
|
||||||
// key-value mappings, where the keys are _not_ encrypted, only the values are.
|
type FileStorageAPI struct {
|
||||||
type AESEncryptedStorage struct {
|
|
||||||
// File to read/write credentials
|
// File to read/write credentials
|
||||||
filename string
|
filename string
|
||||||
// Key stored in base64
|
|
||||||
key []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewAESEncryptedStorage creates a new encrypted storage backed by the given file/key
|
|
||||||
func NewAESEncryptedStorage(filename string, key []byte) *AESEncryptedStorage {
|
|
||||||
return &AESEncryptedStorage{
|
|
||||||
filename: filename,
|
|
||||||
key: key,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Put stores a value by key. 0-length keys results in noop.
|
// Put stores a value by key. 0-length keys results in noop.
|
||||||
func (s *AESEncryptedStorage) Put(key, value string) {
|
func (s *FileStorageAPI) Put(key, value string) {
|
||||||
if len(key) == 0 {
|
if len(key) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
data, err := s.readEncryptedStorage()
|
data, err := s.readStorage()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("Failed to read encrypted storage", "err", err, "file", s.filename)
|
log.Warn("Failed to read encrypted storage", "err", err, "file", s.filename)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ciphertext, iv, err := Encrypt(s.key, []byte(value), []byte(key))
|
data[key] = value
|
||||||
if err != nil {
|
if err = s.writeStorage(data); err != nil {
|
||||||
log.Warn("Failed to encrypt entry", "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
encrypted := StoredCredential{Iv: iv, CipherText: ciphertext}
|
|
||||||
data[key] = encrypted
|
|
||||||
if err = s.writeEncryptedStorage(data); err != nil {
|
|
||||||
log.Warn("Failed to write entry", "err", err)
|
log.Warn("Failed to write entry", "err", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get returns the previously stored value, or an error if it does not exist or
|
// Get returns the previously stored value, or an error if it does not exist or
|
||||||
// key is of 0-length.
|
// key is of 0-length.
|
||||||
func (s *AESEncryptedStorage) Get(key string) (string, error) {
|
func (s *FileStorageAPI) Get(key string) (string, error) {
|
||||||
if len(key) == 0 {
|
if len(key) == 0 {
|
||||||
return "", ErrZeroKey
|
return "", ErrZeroKey
|
||||||
}
|
}
|
||||||
data, err := s.readEncryptedStorage()
|
data, err := s.readStorage()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("Failed to read encrypted storage", "err", err, "file", s.filename)
|
log.Warn("Failed to read encrypted storage", "err", err, "file", s.filename)
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
encrypted, exist := data[key]
|
value, exist := data[key]
|
||||||
if !exist {
|
if !exist {
|
||||||
log.Warn("Key does not exist", "key", key)
|
log.Warn("Key does not exist", "key", key)
|
||||||
return "", ErrNotFound
|
return "", ErrNotFound
|
||||||
}
|
}
|
||||||
entry, err := Decrypt(s.key, encrypted.Iv, encrypted.CipherText, []byte(key))
|
return value, nil
|
||||||
if err != nil {
|
|
||||||
log.Warn("Failed to decrypt key", "key", key)
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return string(entry), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Del removes a key-value pair. If the key doesn't exist, the method is a noop.
|
// Del removes a key-value pair. If the key doesn't exist, the method is a noop.
|
||||||
func (s *AESEncryptedStorage) Del(key string) {
|
func (s *FileStorageAPI) Del(key string) {
|
||||||
data, err := s.readEncryptedStorage()
|
data, err := s.readStorage()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("Failed to read encrypted storage", "err", err, "file", s.filename)
|
log.Warn("Failed to read encrypted storage", "err", err, "file", s.filename)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
delete(data, key)
|
delete(data, key)
|
||||||
if err = s.writeEncryptedStorage(data); err != nil {
|
if err = s.writeStorage(data); err != nil {
|
||||||
log.Warn("Failed to write entry", "err", err)
|
log.Warn("Failed to write entry", "err", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// readEncryptedStorage reads the file with encrypted creds
|
// readEncryptedStorage reads the file with encrypted creds
|
||||||
func (s *AESEncryptedStorage) readEncryptedStorage() (map[string]StoredCredential, error) {
|
// func (s *FileStorageAPI) readEncryptedStorage() (map[string]StoredCredential, error) {
|
||||||
creds := make(map[string]StoredCredential)
|
// creds := make(map[string]StoredCredential)
|
||||||
|
// raw, err := ioutil.ReadFile(s.filename)
|
||||||
|
|
||||||
|
// if err != nil {
|
||||||
|
// if os.IsNotExist(err) {
|
||||||
|
// // Doesn't exist yet
|
||||||
|
// return creds, nil
|
||||||
|
// }
|
||||||
|
// log.Warn("Failed to read encrypted storage", "err", err, "file", s.filename)
|
||||||
|
// }
|
||||||
|
// if err = json.Unmarshal(raw, &creds); err != nil {
|
||||||
|
// log.Warn("Failed to unmarshal encrypted storage", "err", err, "file", s.filename)
|
||||||
|
// return nil, err
|
||||||
|
// }
|
||||||
|
// return creds, nil
|
||||||
|
// }
|
||||||
|
|
||||||
|
// readStorage reads the file with encrypted creds and return them as is
|
||||||
|
func (s *FileStorageAPI) readStorage() (map[string]string, error) {
|
||||||
|
creds := make(map[string]string)
|
||||||
raw, err := ioutil.ReadFile(s.filename)
|
raw, err := ioutil.ReadFile(s.filename)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -110,17 +107,29 @@ func (s *AESEncryptedStorage) readEncryptedStorage() (map[string]StoredCredentia
|
||||||
// Doesn't exist yet
|
// Doesn't exist yet
|
||||||
return creds, nil
|
return creds, nil
|
||||||
}
|
}
|
||||||
log.Warn("Failed to read encrypted storage", "err", err, "file", s.filename)
|
log.Warn("Failed to read file storage", "err", err, "file", s.filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = json.Unmarshal(raw, &creds); err != nil {
|
if err = json.Unmarshal(raw, &creds); err != nil {
|
||||||
log.Warn("Failed to unmarshal encrypted storage", "err", err, "file", s.filename)
|
log.Warn("Failed to unmarshal file storage", "err", err, "file", s.filename)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return creds, nil
|
return creds, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeEncryptedStorage write the file with encrypted creds
|
// writeEncryptedStorage write the file with encrypted creds
|
||||||
func (s *AESEncryptedStorage) writeEncryptedStorage(creds map[string]StoredCredential) error {
|
// func (s *FileStorageAPI) writeEncryptedStorage(creds map[string]StoredCredential) error {
|
||||||
|
// raw, err := json.Marshal(creds)
|
||||||
|
// if err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
// if err = ioutil.WriteFile(s.filename, raw, 0600); err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
|
||||||
|
func (s *FileStorageAPI) writeStorage(creds map[string]string) error {
|
||||||
raw, err := json.Marshal(creds)
|
raw, err := json.Marshal(creds)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -17,42 +17,32 @@
|
||||||
package storage
|
package storage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
"github.com/ethereum/go-ethereum/log"
|
||||||
"github.com/mattn/go-colorable"
|
"github.com/mattn/go-colorable"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestFileStorage(t *testing.T) {
|
func TestFileStorageAPI(t *testing.T) {
|
||||||
a := map[string]StoredCredential{
|
a := map[string]string{
|
||||||
"secret": {
|
"secret": "value1",
|
||||||
Iv: common.Hex2Bytes("cdb30036279601aeee60f16b"),
|
"secret2": "value2",
|
||||||
CipherText: common.Hex2Bytes("f311ac49859d7260c2c464c28ffac122daf6be801d3cfd3edcbde7e00c9ff74f"),
|
|
||||||
},
|
|
||||||
"secret2": {
|
|
||||||
Iv: common.Hex2Bytes("afb8a7579bf971db9f8ceeed"),
|
|
||||||
CipherText: common.Hex2Bytes("2df87baf86b5073ef1f03e3cc738de75b511400f5465bb0ddeacf47ae4dc267d"),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
d, err := ioutil.TempDir("", "eth-encrypted-storage-test")
|
d, err := ioutil.TempDir("", "eth-encrypted-storage-test")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
stored := &AESEncryptedStorage{
|
stored := &FileStorageAPI{
|
||||||
filename: fmt.Sprintf("%v/vault.json", d),
|
filename: fmt.Sprintf("%v/vault.json", d),
|
||||||
key: []byte("AES256Key-32Characters1234567890"),
|
|
||||||
}
|
}
|
||||||
stored.writeEncryptedStorage(a)
|
stored.writeStorage(a)
|
||||||
read := &AESEncryptedStorage{
|
read := &FileStorageAPI{
|
||||||
filename: fmt.Sprintf("%v/vault.json", d),
|
filename: fmt.Sprintf("%v/vault.json", d),
|
||||||
key: []byte("AES256Key-32Characters1234567890"),
|
|
||||||
}
|
}
|
||||||
creds, err := read.readEncryptedStorage()
|
creds, err := read.readStorage()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
@ -60,15 +50,16 @@ func TestFileStorage(t *testing.T) {
|
||||||
if v2, exist := creds[k]; !exist {
|
if v2, exist := creds[k]; !exist {
|
||||||
t.Errorf("Missing entry %v", k)
|
t.Errorf("Missing entry %v", k)
|
||||||
} else {
|
} else {
|
||||||
if !bytes.Equal(v.CipherText, v2.CipherText) {
|
if v != v2 {
|
||||||
t.Errorf("Wrong ciphertext, expected %x got %x", v.CipherText, v2.CipherText)
|
t.Errorf("Wrong ciphertext, expected %x got %x", v, v2)
|
||||||
}
|
}
|
||||||
if !bytes.Equal(v.Iv, v2.Iv) {
|
if v != v2 {
|
||||||
t.Errorf("Wrong iv")
|
t.Errorf("Wrong iv")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEnd2End(t *testing.T) {
|
func TestEnd2End(t *testing.T) {
|
||||||
log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(3), log.StreamHandler(colorable.NewColorableStderr(), log.TerminalFormat(true))))
|
log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(3), log.StreamHandler(colorable.NewColorableStderr(), log.TerminalFormat(true))))
|
||||||
|
|
||||||
|
|
@ -77,17 +68,21 @@ func TestEnd2End(t *testing.T) {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
s1 := &AESEncryptedStorage{
|
filename := fmt.Sprintf("%v/vault.json", d)
|
||||||
filename: fmt.Sprintf("%v/vault.json", d),
|
key := []byte("AES256Key-32Characters1234567890")
|
||||||
key: []byte("AES256Key-32Characters1234567890"),
|
fs := NewFileStorage(filename, key)
|
||||||
}
|
|
||||||
s2 := &AESEncryptedStorage{
|
fs.Put("bazonk", "foobar")
|
||||||
filename: fmt.Sprintf("%v/vault.json", d),
|
|
||||||
key: []byte("AES256Key-32Characters1234567890"),
|
// make sure intermediate result is encrypted correctly
|
||||||
|
encrypted, err := fs.api.Get("bazonk")
|
||||||
|
cred := StoredCredential{}
|
||||||
|
if err = json.Unmarshal([]byte(encrypted), &cred); err != nil {
|
||||||
|
t.Error("Failed to unmarshal encrypted credential", "err", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
s1.Put("bazonk", "foobar")
|
// make sure return is correct
|
||||||
if v, err := s2.Get("bazonk"); v != "foobar" || err != nil {
|
if v, err := fs.Get("bazonk"); v != "foobar" || err != nil {
|
||||||
t.Errorf("Expected bazonk->foobar (nil error), got '%v' (%v error)", v, err)
|
t.Errorf("Expected bazonk->foobar (nil error), got '%v' (%v error)", v, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -102,16 +97,15 @@ func TestSwappedKeys(t *testing.T) {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
s1 := &AESEncryptedStorage{
|
filename := fmt.Sprintf("%v/vault.json", d)
|
||||||
filename: fmt.Sprintf("%v/vault.json", d),
|
key := []byte("AES256Key-32Characters1234567890")
|
||||||
key: []byte("AES256Key-32Characters1234567890"),
|
s1 := NewFileStorage(filename, key)
|
||||||
}
|
|
||||||
s1.Put("k1", "v1")
|
s1.Put("k1", "v1")
|
||||||
s1.Put("k2", "v2")
|
s1.Put("k2", "v2")
|
||||||
// Now make a modified copy
|
// Now make a modified copy
|
||||||
|
|
||||||
creds := make(map[string]StoredCredential)
|
creds := make(map[string]string)
|
||||||
raw, err := ioutil.ReadFile(s1.filename)
|
raw, err := ioutil.ReadFile(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
@ -126,7 +120,7 @@ func TestSwappedKeys(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err = ioutil.WriteFile(s1.filename, raw, 0600); err != nil {
|
if err = ioutil.WriteFile(filename, raw, 0600); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
17
signer/storage/no_storage.go
Normal file
17
signer/storage/no_storage.go
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
package storage
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
// NoStorageAPI is a dummy construct which doesn't remember anything you tell it
|
||||||
|
type NoStorageAPI struct{}
|
||||||
|
|
||||||
|
// Put is a dummy function that do nothing
|
||||||
|
func (s *NoStorageAPI) Put(key, value string) {}
|
||||||
|
|
||||||
|
// Del is a dummy function that do nothing
|
||||||
|
func (s *NoStorageAPI) Del(key string) {}
|
||||||
|
|
||||||
|
// Get is a dummy function that do nothing
|
||||||
|
func (s *NoStorageAPI) Get(key string) (string, error) {
|
||||||
|
return "", errors.New("missing key, I probably forgot")
|
||||||
|
}
|
||||||
|
|
@ -16,7 +16,13 @@
|
||||||
|
|
||||||
package storage
|
package storage
|
||||||
|
|
||||||
import "errors"
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/cmd/clef/dbutil"
|
||||||
|
"github.com/ethereum/go-ethereum/log"
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// ErrZeroKey is returned if an attempt was made to inset a 0-length key.
|
// ErrZeroKey is returned if an attempt was made to inset a 0-length key.
|
||||||
|
|
@ -26,7 +32,8 @@ var (
|
||||||
ErrNotFound = errors.New("not found")
|
ErrNotFound = errors.New("not found")
|
||||||
)
|
)
|
||||||
|
|
||||||
type Storage interface {
|
// storageAPI is the interface that defines interactions with backend storage client
|
||||||
|
type storageAPI interface {
|
||||||
// Put stores a value by key. 0-length keys results in noop.
|
// Put stores a value by key. 0-length keys results in noop.
|
||||||
Put(key, value string)
|
Put(key, value string)
|
||||||
|
|
||||||
|
|
@ -38,49 +45,109 @@ type Storage interface {
|
||||||
Del(key string)
|
Del(key string)
|
||||||
}
|
}
|
||||||
|
|
||||||
// EphemeralStorage is an in-memory storage that does
|
// Storage is the storage client which is used by client to store key/value mappings.
|
||||||
// not persist values to disk. Mainly used for testing
|
// The keys are _not_ encrypted, only the values are.
|
||||||
type EphemeralStorage struct {
|
type Storage struct {
|
||||||
data map[string]string
|
api storageAPI
|
||||||
|
key []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
// Put stores a value by key. 0-length keys results in noop.
|
// Get calls the underlying storageApi's Get function and then decrypts the value field
|
||||||
func (s *EphemeralStorage) Put(key, value string) {
|
func (s *Storage) Get(key string) (string, error) {
|
||||||
if len(key) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
s.data[key] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get returns the previously stored value, or an error if the key is 0-length
|
|
||||||
// or unknown.
|
|
||||||
func (s *EphemeralStorage) Get(key string) (string, error) {
|
|
||||||
if len(key) == 0 {
|
if len(key) == 0 {
|
||||||
return "", ErrZeroKey
|
return "", ErrZeroKey
|
||||||
}
|
}
|
||||||
if v, ok := s.data[key]; ok {
|
|
||||||
return v, nil
|
data, err := s.api.Get(key)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
}
|
}
|
||||||
return "", ErrNotFound
|
|
||||||
|
cred := StoredCredential{}
|
||||||
|
if err = json.Unmarshal([]byte(data), &cred); err != nil {
|
||||||
|
log.Warn("Failed to unmarshal encrypted credential", "err", err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
entry, err := Decrypt(s.key, cred.Iv, cred.CipherText, []byte(key))
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("Failed to decrypt key", "key", key)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(entry), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Del removes a key-value pair. If the key doesn't exist, the method is a noop.
|
// Put encrypts the value field with key as additionalData to prevent value swap attack.
|
||||||
func (s *EphemeralStorage) Del(key string) {
|
// Then calls the underlying storageApi's Put function to persist the key/value pair
|
||||||
delete(s.data, key)
|
func (s *Storage) Put(key, value string) {
|
||||||
|
if len(key) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ciphertext, iv, err := Encrypt(s.key, []byte(value), []byte(key))
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("Failed to encrypt entry", "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
encrypted := StoredCredential{Iv: iv, CipherText: ciphertext}
|
||||||
|
raw, err := json.Marshal(encrypted)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("Failed to marshal credential", "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.api.Put(key, string(raw))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Del calls the underlying storageApi's Del function to delete the key/value pair
|
||||||
|
func (s *Storage) Del(key string) {
|
||||||
|
s.api.Del(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEphemeralStorage creates an in-memory storage that does
|
||||||
|
// not persist values to disk. Mainly used for testing
|
||||||
func NewEphemeralStorage() Storage {
|
func NewEphemeralStorage() Storage {
|
||||||
s := &EphemeralStorage{
|
api := &EphemeralStorageAPI{
|
||||||
data: make(map[string]string),
|
data: make(map[string]string),
|
||||||
}
|
}
|
||||||
return s
|
return Storage{
|
||||||
|
api: api,
|
||||||
|
key: []byte(""),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NoStorage is a dummy construct which doesn't remember anything you tell it
|
// NewNoStorage creates an dummy storage which didn't remember anything you tell it
|
||||||
type NoStorage struct{}
|
func NewNoStorage() Storage {
|
||||||
|
api := &NoStorageAPI{}
|
||||||
func (s *NoStorage) Put(key, value string) {}
|
return Storage{
|
||||||
func (s *NoStorage) Del(key string) {}
|
api: api,
|
||||||
func (s *NoStorage) Get(key string) (string, error) {
|
key: []byte(""),
|
||||||
return "", errors.New("missing key, I probably forgot")
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDBStorage creates a database storage
|
||||||
|
func NewDBStorage(path, table string, key []byte) (*Storage, error) {
|
||||||
|
kvstore, err := dbutil.NewKVStore(path, table)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
api := &DBStorageAPI{
|
||||||
|
kvstore: kvstore,
|
||||||
|
}
|
||||||
|
return &Storage{
|
||||||
|
api: api,
|
||||||
|
key: key,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFileStorage creates a storage type which is backed by a json-file.
|
||||||
|
func NewFileStorage(filename string, key []byte) *Storage {
|
||||||
|
api := &FileStorageAPI{
|
||||||
|
filename: filename,
|
||||||
|
}
|
||||||
|
return &Storage{
|
||||||
|
api: api,
|
||||||
|
key: key,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue