aboutsummaryrefslogtreecommitdiffstats
path: root/accounts
diff options
context:
space:
mode:
Diffstat (limited to 'accounts')
-rw-r--r--accounts/account_manager.go164
-rw-r--r--accounts/accounts_test.go63
2 files changed, 146 insertions, 81 deletions
diff --git a/accounts/account_manager.go b/accounts/account_manager.go
index 3e9fa7799..646dc8376 100644
--- a/accounts/account_manager.go
+++ b/accounts/account_manager.go
@@ -33,7 +33,10 @@ and accounts persistence is derived from stored keys' addresses
package accounts
import (
+ "bytes"
+ "crypto/ecdsa"
crand "crypto/rand"
+ "os"
"errors"
"sync"
@@ -42,77 +45,117 @@ import (
"github.com/ethereum/go-ethereum/crypto"
)
-var ErrLocked = errors.New("account is locked; please request passphrase")
+var (
+ ErrLocked = errors.New("account is locked")
+ ErrNoKeys = errors.New("no keys in store")
+)
-// TODO: better name for this struct?
type Account struct {
Address []byte
}
-type AccountManager struct {
- keyStore crypto.KeyStore2
- unlockedKeys map[string]crypto.Key
- unlockMilliseconds time.Duration
- mutex sync.RWMutex
+type Manager struct {
+ keyStore crypto.KeyStore2
+ unlocked map[string]*unlocked
+ mutex sync.RWMutex
+}
+
+type unlocked struct {
+ *crypto.Key
+ abort chan struct{}
+}
+
+func NewManager(keyStore crypto.KeyStore2) *Manager {
+ return &Manager{
+ keyStore: keyStore,
+ unlocked: make(map[string]*unlocked),
+ }
+}
+
+func (am *Manager) HasAccount(addr []byte) bool {
+ accounts, _ := am.Accounts()
+ for _, acct := range accounts {
+ if bytes.Compare(acct.Address, addr) == 0 {
+ return true
+ }
+ }
+ return false
+}
+
+// Coinbase returns the account address that mining rewards are sent to.
+func (am *Manager) Coinbase() (addr []byte, err error) {
+ // TODO: persist coinbase address on disk
+ return am.firstAddr()
}
-func NewAccountManager(keyStore crypto.KeyStore2, unlockMilliseconds time.Duration) AccountManager {
- keysMap := make(map[string]crypto.Key)
- am := &AccountManager{
- keyStore: keyStore,
- unlockedKeys: keysMap,
- unlockMilliseconds: unlockMilliseconds,
+func (am *Manager) firstAddr() ([]byte, error) {
+ addrs, err := am.keyStore.GetKeyAddresses()
+ if os.IsNotExist(err) {
+ return nil, ErrNoKeys
+ } else if err != nil {
+ return nil, err
+ }
+ if len(addrs) == 0 {
+ return nil, ErrNoKeys
}
- return *am
+ return addrs[0], nil
}
-func (am AccountManager) DeleteAccount(address []byte, auth string) error {
+func (am *Manager) DeleteAccount(address []byte, auth string) error {
return am.keyStore.DeleteKey(address, auth)
}
-func (am *AccountManager) Sign(fromAccount *Account, toSign []byte) (signature []byte, err error) {
+func (am *Manager) Sign(a Account, toSign []byte) (signature []byte, err error) {
am.mutex.RLock()
- unlockedKey := am.unlockedKeys[string(fromAccount.Address)]
+ unlockedKey, found := am.unlocked[string(a.Address)]
am.mutex.RUnlock()
- if unlockedKey.Address == nil {
+ if !found {
return nil, ErrLocked
}
signature, err = crypto.Sign(toSign, unlockedKey.PrivateKey)
return signature, err
}
-func (am *AccountManager) SignLocked(fromAccount *Account, keyAuth string, toSign []byte) (signature []byte, err error) {
- key, err := am.keyStore.GetKey(fromAccount.Address, keyAuth)
+// TimedUnlock unlocks the account with the given address.
+// When timeout has passed, the account will be locked again.
+func (am *Manager) TimedUnlock(addr []byte, keyAuth string, timeout time.Duration) error {
+ key, err := am.keyStore.GetKey(addr, keyAuth)
if err != nil {
- return nil, err
+ return err
}
- am.mutex.RLock()
- am.unlockedKeys[string(fromAccount.Address)] = *key
- am.mutex.RUnlock()
- go unlockLater(am, fromAccount.Address)
- signature, err = crypto.Sign(toSign, key.PrivateKey)
- return signature, err
+ u := am.addUnlocked(addr, key)
+ go am.dropLater(addr, u, timeout)
+ return nil
}
-func (am AccountManager) NewAccount(auth string) (*Account, error) {
- key, err := am.keyStore.GenerateNewKey(crand.Reader, auth)
+// Unlock unlocks the account with the given address. The account
+// stays unlocked until the program exits or until a TimedUnlock
+// timeout (started after the call to Unlock) expires.
+func (am *Manager) Unlock(addr []byte, keyAuth string) error {
+ key, err := am.keyStore.GetKey(addr, keyAuth)
if err != nil {
- return nil, err
+ return err
}
- ua := &Account{
- Address: key.Address,
+ am.addUnlocked(addr, key)
+ return nil
+}
+
+func (am *Manager) NewAccount(auth string) (Account, error) {
+ key, err := am.keyStore.GenerateNewKey(crand.Reader, auth)
+ if err != nil {
+ return Account{}, err
}
- return ua, err
+ return Account{Address: key.Address}, nil
}
-func (am *AccountManager) Accounts() ([]Account, error) {
+func (am *Manager) Accounts() ([]Account, error) {
addresses, err := am.keyStore.GetKeyAddresses()
- if err != nil {
+ if os.IsNotExist(err) {
+ return nil, ErrNoKeys
+ } else if err != nil {
return nil, err
}
-
accounts := make([]Account, len(addresses))
-
for i, addr := range addresses {
accounts[i] = Account{
Address: addr,
@@ -121,12 +164,47 @@ func (am *AccountManager) Accounts() ([]Account, error) {
return accounts, err
}
-func unlockLater(am *AccountManager, addr []byte) {
+func (am *Manager) addUnlocked(addr []byte, key *crypto.Key) *unlocked {
+ u := &unlocked{Key: key, abort: make(chan struct{})}
+ am.mutex.Lock()
+ prev, found := am.unlocked[string(addr)]
+ if found {
+ // terminate dropLater for this key to avoid unexpected drops.
+ close(prev.abort)
+ // the key is zeroed here instead of in dropLater because
+ // there might not actually be a dropLater running for this
+ // key, i.e. when Unlock was used.
+ zeroKey(prev.PrivateKey)
+ }
+ am.unlocked[string(addr)] = u
+ am.mutex.Unlock()
+ return u
+}
+
+func (am *Manager) dropLater(addr []byte, u *unlocked, timeout time.Duration) {
+ t := time.NewTimer(timeout)
+ defer t.Stop()
select {
- case <-time.After(time.Millisecond * am.unlockMilliseconds):
+ case <-u.abort:
+ // just quit
+ case <-t.C:
+ am.mutex.Lock()
+ // only drop if it's still the same key instance that dropLater
+ // was launched with. we can check that using pointer equality
+ // because the map stores a new pointer every time the key is
+ // unlocked.
+ if am.unlocked[string(addr)] == u {
+ zeroKey(u.PrivateKey)
+ delete(am.unlocked, string(addr))
+ }
+ am.mutex.Unlock()
+ }
+}
+
+// zeroKey zeroes a private key in memory.
+func zeroKey(k *ecdsa.PrivateKey) {
+ b := k.D.Bits()
+ for i := range b {
+ b[i] = 0
}
- am.mutex.RLock()
- // TODO: how do we know the key is actually gone from memory?
- delete(am.unlockedKeys, string(addr))
- am.mutex.RUnlock()
}
diff --git a/accounts/accounts_test.go b/accounts/accounts_test.go
index 44d1d72f1..427114cbd 100644
--- a/accounts/accounts_test.go
+++ b/accounts/accounts_test.go
@@ -1,43 +1,36 @@
package accounts
import (
+ "io/ioutil"
+ "os"
"testing"
+ "time"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/crypto/randentropy"
- "github.com/ethereum/go-ethereum/ethutil"
- "time"
)
-func TestAccountManager(t *testing.T) {
- ks := crypto.NewKeyStorePlain(ethutil.DefaultDataDir() + "/testaccounts")
- am := NewAccountManager(ks, 100)
+func TestSign(t *testing.T) {
+ dir, ks := tmpKeyStore(t, crypto.NewKeyStorePlain)
+ defer os.RemoveAll(dir)
+
+ am := NewManager(ks)
pass := "" // not used but required by API
a1, err := am.NewAccount(pass)
toSign := randentropy.GetEntropyCSPRNG(32)
- _, err = am.SignLocked(a1, pass, toSign)
- if err != nil {
- t.Fatal(err)
- }
-
- // Cleanup
- time.Sleep(time.Millisecond * 150) // wait for locking
+ am.Unlock(a1.Address, "")
- accounts, err := am.Accounts()
+ _, err = am.Sign(a1, toSign)
if err != nil {
t.Fatal(err)
}
- for _, account := range accounts {
- err := am.DeleteAccount(account.Address, pass)
- if err != nil {
- t.Fatal(err)
- }
- }
}
-func TestAccountManagerLocking(t *testing.T) {
- ks := crypto.NewKeyStorePassphrase(ethutil.DefaultDataDir() + "/testaccounts")
- am := NewAccountManager(ks, 200)
+func TestTimedUnlock(t *testing.T) {
+ dir, ks := tmpKeyStore(t, crypto.NewKeyStorePassphrase)
+ defer os.RemoveAll(dir)
+
+ am := NewManager(ks)
pass := "foo"
a1, err := am.NewAccount(pass)
toSign := randentropy.GetEntropyCSPRNG(32)
@@ -45,38 +38,32 @@ func TestAccountManagerLocking(t *testing.T) {
// Signing without passphrase fails because account is locked
_, err = am.Sign(a1, toSign)
if err != ErrLocked {
- t.Fatal(err)
+ t.Fatal("Signing should've failed with ErrLocked before unlocking, got ", err)
}
// Signing with passphrase works
- _, err = am.SignLocked(a1, pass, toSign)
- if err != nil {
+ if err = am.TimedUnlock(a1.Address, pass, 100*time.Millisecond); err != nil {
t.Fatal(err)
}
// Signing without passphrase works because account is temp unlocked
_, err = am.Sign(a1, toSign)
if err != nil {
- t.Fatal(err)
+ t.Fatal("Signing shouldn't return an error after unlocking, got ", err)
}
- // Signing without passphrase fails after automatic locking
- time.Sleep(time.Millisecond * time.Duration(250))
-
+ // Signing fails again after automatic locking
+ time.Sleep(150 * time.Millisecond)
_, err = am.Sign(a1, toSign)
if err != ErrLocked {
- t.Fatal(err)
+ t.Fatal("Signing should've failed with ErrLocked timeout expired, got ", err)
}
+}
- // Cleanup
- accounts, err := am.Accounts()
+func tmpKeyStore(t *testing.T, new func(string) crypto.KeyStore2) (string, crypto.KeyStore2) {
+ d, err := ioutil.TempDir("", "eth-keystore-test")
if err != nil {
t.Fatal(err)
}
- for _, account := range accounts {
- err := am.DeleteAccount(account.Address, pass)
- if err != nil {
- t.Fatal(err)
- }
- }
+ return d, new(d)
}