accounts, cmd, eth, internal, mobile, node: split account backends
This commit is contained in:
271
accounts/keystore/address_cache.go
Normal file
271
accounts/keystore/address_cache.go
Normal file
@ -0,0 +1,271 @@
|
||||
// Copyright 2016 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package keystore
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/logger"
|
||||
"github.com/ethereum/go-ethereum/logger/glog"
|
||||
)
|
||||
|
||||
// Minimum amount of time between cache reloads. This limit applies if the platform does
|
||||
// not support change notifications. It also applies if the keystore directory does not
|
||||
// exist yet, the code will attempt to create a watcher at most this often.
|
||||
const minReloadInterval = 2 * time.Second
|
||||
|
||||
type accountsByFile []accounts.Account
|
||||
|
||||
func (s accountsByFile) Len() int { return len(s) }
|
||||
func (s accountsByFile) Less(i, j int) bool { return s[i].URL < s[j].URL }
|
||||
func (s accountsByFile) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
||||
|
||||
// AmbiguousAddrError is returned when attempting to unlock
|
||||
// an address for which more than one file exists.
|
||||
type AmbiguousAddrError struct {
|
||||
Addr common.Address
|
||||
Matches []accounts.Account
|
||||
}
|
||||
|
||||
func (err *AmbiguousAddrError) Error() string {
|
||||
files := ""
|
||||
for i, a := range err.Matches {
|
||||
files += a.URL
|
||||
if i < len(err.Matches)-1 {
|
||||
files += ", "
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("multiple keys match address (%s)", files)
|
||||
}
|
||||
|
||||
// addressCache is a live index of all accounts in the keystore.
|
||||
type addressCache struct {
|
||||
keydir string
|
||||
watcher *watcher
|
||||
mu sync.Mutex
|
||||
all accountsByFile
|
||||
byAddr map[common.Address][]accounts.Account
|
||||
throttle *time.Timer
|
||||
}
|
||||
|
||||
func newAddrCache(keydir string) *addressCache {
|
||||
ac := &addressCache{
|
||||
keydir: keydir,
|
||||
byAddr: make(map[common.Address][]accounts.Account),
|
||||
}
|
||||
ac.watcher = newWatcher(ac)
|
||||
return ac
|
||||
}
|
||||
|
||||
func (ac *addressCache) accounts() []accounts.Account {
|
||||
ac.maybeReload()
|
||||
ac.mu.Lock()
|
||||
defer ac.mu.Unlock()
|
||||
cpy := make([]accounts.Account, len(ac.all))
|
||||
copy(cpy, ac.all)
|
||||
return cpy
|
||||
}
|
||||
|
||||
func (ac *addressCache) hasAddress(addr common.Address) bool {
|
||||
ac.maybeReload()
|
||||
ac.mu.Lock()
|
||||
defer ac.mu.Unlock()
|
||||
return len(ac.byAddr[addr]) > 0
|
||||
}
|
||||
|
||||
func (ac *addressCache) add(newAccount accounts.Account) {
|
||||
ac.mu.Lock()
|
||||
defer ac.mu.Unlock()
|
||||
|
||||
i := sort.Search(len(ac.all), func(i int) bool { return ac.all[i].URL >= newAccount.URL })
|
||||
if i < len(ac.all) && ac.all[i] == newAccount {
|
||||
return
|
||||
}
|
||||
// newAccount is not in the cache.
|
||||
ac.all = append(ac.all, accounts.Account{})
|
||||
copy(ac.all[i+1:], ac.all[i:])
|
||||
ac.all[i] = newAccount
|
||||
ac.byAddr[newAccount.Address] = append(ac.byAddr[newAccount.Address], newAccount)
|
||||
}
|
||||
|
||||
// note: removed needs to be unique here (i.e. both File and Address must be set).
|
||||
func (ac *addressCache) delete(removed accounts.Account) {
|
||||
ac.mu.Lock()
|
||||
defer ac.mu.Unlock()
|
||||
ac.all = removeAccount(ac.all, removed)
|
||||
if ba := removeAccount(ac.byAddr[removed.Address], removed); len(ba) == 0 {
|
||||
delete(ac.byAddr, removed.Address)
|
||||
} else {
|
||||
ac.byAddr[removed.Address] = ba
|
||||
}
|
||||
}
|
||||
|
||||
func removeAccount(slice []accounts.Account, elem accounts.Account) []accounts.Account {
|
||||
for i := range slice {
|
||||
if slice[i] == elem {
|
||||
return append(slice[:i], slice[i+1:]...)
|
||||
}
|
||||
}
|
||||
return slice
|
||||
}
|
||||
|
||||
// find returns the cached account for address if there is a unique match.
|
||||
// The exact matching rules are explained by the documentation of accounts.Account.
|
||||
// Callers must hold ac.mu.
|
||||
func (ac *addressCache) find(a accounts.Account) (accounts.Account, error) {
|
||||
// Limit search to address candidates if possible.
|
||||
matches := ac.all
|
||||
if (a.Address != common.Address{}) {
|
||||
matches = ac.byAddr[a.Address]
|
||||
}
|
||||
if a.URL != "" {
|
||||
// If only the basename is specified, complete the path.
|
||||
if !strings.ContainsRune(a.URL, filepath.Separator) {
|
||||
a.URL = filepath.Join(ac.keydir, a.URL)
|
||||
}
|
||||
for i := range matches {
|
||||
if matches[i].URL == a.URL {
|
||||
return matches[i], nil
|
||||
}
|
||||
}
|
||||
if (a.Address == common.Address{}) {
|
||||
return accounts.Account{}, ErrNoMatch
|
||||
}
|
||||
}
|
||||
switch len(matches) {
|
||||
case 1:
|
||||
return matches[0], nil
|
||||
case 0:
|
||||
return accounts.Account{}, ErrNoMatch
|
||||
default:
|
||||
err := &AmbiguousAddrError{Addr: a.Address, Matches: make([]accounts.Account, len(matches))}
|
||||
copy(err.Matches, matches)
|
||||
return accounts.Account{}, err
|
||||
}
|
||||
}
|
||||
|
||||
func (ac *addressCache) maybeReload() {
|
||||
ac.mu.Lock()
|
||||
defer ac.mu.Unlock()
|
||||
if ac.watcher.running {
|
||||
return // A watcher is running and will keep the cache up-to-date.
|
||||
}
|
||||
if ac.throttle == nil {
|
||||
ac.throttle = time.NewTimer(0)
|
||||
} else {
|
||||
select {
|
||||
case <-ac.throttle.C:
|
||||
default:
|
||||
return // The cache was reloaded recently.
|
||||
}
|
||||
}
|
||||
ac.watcher.start()
|
||||
ac.reload()
|
||||
ac.throttle.Reset(minReloadInterval)
|
||||
}
|
||||
|
||||
func (ac *addressCache) close() {
|
||||
ac.mu.Lock()
|
||||
ac.watcher.close()
|
||||
if ac.throttle != nil {
|
||||
ac.throttle.Stop()
|
||||
}
|
||||
ac.mu.Unlock()
|
||||
}
|
||||
|
||||
// reload caches addresses of existing accounts.
|
||||
// Callers must hold ac.mu.
|
||||
func (ac *addressCache) reload() {
|
||||
accounts, err := ac.scan()
|
||||
if err != nil && glog.V(logger.Debug) {
|
||||
glog.Errorf("can't load keys: %v", err)
|
||||
}
|
||||
ac.all = accounts
|
||||
sort.Sort(ac.all)
|
||||
for k := range ac.byAddr {
|
||||
delete(ac.byAddr, k)
|
||||
}
|
||||
for _, a := range accounts {
|
||||
ac.byAddr[a.Address] = append(ac.byAddr[a.Address], a)
|
||||
}
|
||||
glog.V(logger.Debug).Infof("reloaded keys, cache has %d accounts", len(ac.all))
|
||||
}
|
||||
|
||||
func (ac *addressCache) scan() ([]accounts.Account, error) {
|
||||
files, err := ioutil.ReadDir(ac.keydir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var (
|
||||
buf = new(bufio.Reader)
|
||||
addrs []accounts.Account
|
||||
keyJSON struct {
|
||||
Address string `json:"address"`
|
||||
}
|
||||
)
|
||||
for _, fi := range files {
|
||||
path := filepath.Join(ac.keydir, fi.Name())
|
||||
if skipKeyFile(fi) {
|
||||
glog.V(logger.Detail).Infof("ignoring file %s", path)
|
||||
continue
|
||||
}
|
||||
fd, err := os.Open(path)
|
||||
if err != nil {
|
||||
glog.V(logger.Detail).Infoln(err)
|
||||
continue
|
||||
}
|
||||
buf.Reset(fd)
|
||||
// Parse the address.
|
||||
keyJSON.Address = ""
|
||||
err = json.NewDecoder(buf).Decode(&keyJSON)
|
||||
addr := common.HexToAddress(keyJSON.Address)
|
||||
switch {
|
||||
case err != nil:
|
||||
glog.V(logger.Debug).Infof("can't decode key %s: %v", path, err)
|
||||
case (addr == common.Address{}):
|
||||
glog.V(logger.Debug).Infof("can't decode key %s: missing or zero address", path)
|
||||
default:
|
||||
addrs = append(addrs, accounts.Account{Address: addr, URL: path})
|
||||
}
|
||||
fd.Close()
|
||||
}
|
||||
return addrs, err
|
||||
}
|
||||
|
||||
func skipKeyFile(fi os.FileInfo) bool {
|
||||
// Skip editor backups and UNIX-style hidden files.
|
||||
if strings.HasSuffix(fi.Name(), "~") || strings.HasPrefix(fi.Name(), ".") {
|
||||
return true
|
||||
}
|
||||
// Skip misc special files, directories (yes, symlinks too).
|
||||
if fi.IsDir() || fi.Mode()&os.ModeType != 0 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
284
accounts/keystore/address_cache_test.go
Normal file
284
accounts/keystore/address_cache_test.go
Normal file
@ -0,0 +1,284 @@
|
||||
// Copyright 2016 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package keystore
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"sort"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cespare/cp"
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/ethereum/go-ethereum/accounts"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
)
|
||||
|
||||
var (
|
||||
cachetestDir, _ = filepath.Abs(filepath.Join("testdata", "keystore"))
|
||||
cachetestAccounts = []accounts.Account{
|
||||
{
|
||||
Address: common.HexToAddress("7ef5a6135f1fd6a02593eedc869c6d41d934aef8"),
|
||||
URL: filepath.Join(cachetestDir, "UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8"),
|
||||
},
|
||||
{
|
||||
Address: common.HexToAddress("f466859ead1932d743d622cb74fc058882e8648a"),
|
||||
URL: filepath.Join(cachetestDir, "aaa"),
|
||||
},
|
||||
{
|
||||
Address: common.HexToAddress("289d485d9771714cce91d3393d764e1311907acc"),
|
||||
URL: filepath.Join(cachetestDir, "zzz"),
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func TestWatchNewFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dir, am := tmpKeyStore(t, false)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
// Ensure the watcher is started before adding any files.
|
||||
am.Accounts()
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
// Move in the files.
|
||||
wantAccounts := make([]accounts.Account, len(cachetestAccounts))
|
||||
for i := range cachetestAccounts {
|
||||
a := cachetestAccounts[i]
|
||||
a.URL = filepath.Join(dir, filepath.Base(a.URL))
|
||||
wantAccounts[i] = a
|
||||
if err := cp.CopyFile(a.URL, cachetestAccounts[i].URL); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// am should see the accounts.
|
||||
var list []accounts.Account
|
||||
for d := 200 * time.Millisecond; d < 5*time.Second; d *= 2 {
|
||||
list = am.Accounts()
|
||||
if reflect.DeepEqual(list, wantAccounts) {
|
||||
return
|
||||
}
|
||||
time.Sleep(d)
|
||||
}
|
||||
t.Errorf("got %s, want %s", spew.Sdump(list), spew.Sdump(wantAccounts))
|
||||
}
|
||||
|
||||
func TestWatchNoDir(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Create am but not the directory that it watches.
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
dir := filepath.Join(os.TempDir(), fmt.Sprintf("eth-keystore-watch-test-%d-%d", os.Getpid(), rand.Int()))
|
||||
am := NewKeyStore(dir, LightScryptN, LightScryptP)
|
||||
|
||||
list := am.Accounts()
|
||||
if len(list) > 0 {
|
||||
t.Error("initial account list not empty:", list)
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Create the directory and copy a key file into it.
|
||||
os.MkdirAll(dir, 0700)
|
||||
defer os.RemoveAll(dir)
|
||||
file := filepath.Join(dir, "aaa")
|
||||
if err := cp.CopyFile(file, cachetestAccounts[0].URL); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// am should see the account.
|
||||
wantAccounts := []accounts.Account{cachetestAccounts[0]}
|
||||
wantAccounts[0].URL = file
|
||||
for d := 200 * time.Millisecond; d < 8*time.Second; d *= 2 {
|
||||
list = am.Accounts()
|
||||
if reflect.DeepEqual(list, wantAccounts) {
|
||||
return
|
||||
}
|
||||
time.Sleep(d)
|
||||
}
|
||||
t.Errorf("\ngot %v\nwant %v", list, wantAccounts)
|
||||
}
|
||||
|
||||
func TestCacheInitialReload(t *testing.T) {
|
||||
cache := newAddrCache(cachetestDir)
|
||||
accounts := cache.accounts()
|
||||
if !reflect.DeepEqual(accounts, cachetestAccounts) {
|
||||
t.Fatalf("got initial accounts: %swant %s", spew.Sdump(accounts), spew.Sdump(cachetestAccounts))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCacheAddDeleteOrder(t *testing.T) {
|
||||
cache := newAddrCache("testdata/no-such-dir")
|
||||
cache.watcher.running = true // prevent unexpected reloads
|
||||
|
||||
accs := []accounts.Account{
|
||||
{
|
||||
Address: common.HexToAddress("095e7baea6a6c7c4c2dfeb977efac326af552d87"),
|
||||
URL: "-309830980",
|
||||
},
|
||||
{
|
||||
Address: common.HexToAddress("2cac1adea150210703ba75ed097ddfe24e14f213"),
|
||||
URL: "ggg",
|
||||
},
|
||||
{
|
||||
Address: common.HexToAddress("8bda78331c916a08481428e4b07c96d3e916d165"),
|
||||
URL: "zzzzzz-the-very-last-one.keyXXX",
|
||||
},
|
||||
{
|
||||
Address: common.HexToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"),
|
||||
URL: "SOMETHING.key",
|
||||
},
|
||||
{
|
||||
Address: common.HexToAddress("7ef5a6135f1fd6a02593eedc869c6d41d934aef8"),
|
||||
URL: "UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8",
|
||||
},
|
||||
{
|
||||
Address: common.HexToAddress("f466859ead1932d743d622cb74fc058882e8648a"),
|
||||
URL: "aaa",
|
||||
},
|
||||
{
|
||||
Address: common.HexToAddress("289d485d9771714cce91d3393d764e1311907acc"),
|
||||
URL: "zzz",
|
||||
},
|
||||
}
|
||||
for _, a := range accs {
|
||||
cache.add(a)
|
||||
}
|
||||
// Add some of them twice to check that they don't get reinserted.
|
||||
cache.add(accs[0])
|
||||
cache.add(accs[2])
|
||||
|
||||
// Check that the account list is sorted by filename.
|
||||
wantAccounts := make([]accounts.Account, len(accs))
|
||||
copy(wantAccounts, accs)
|
||||
sort.Sort(accountsByFile(wantAccounts))
|
||||
list := cache.accounts()
|
||||
if !reflect.DeepEqual(list, wantAccounts) {
|
||||
t.Fatalf("got accounts: %s\nwant %s", spew.Sdump(accs), spew.Sdump(wantAccounts))
|
||||
}
|
||||
for _, a := range accs {
|
||||
if !cache.hasAddress(a.Address) {
|
||||
t.Errorf("expected hasAccount(%x) to return true", a.Address)
|
||||
}
|
||||
}
|
||||
if cache.hasAddress(common.HexToAddress("fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e")) {
|
||||
t.Errorf("expected hasAccount(%x) to return false", common.HexToAddress("fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e"))
|
||||
}
|
||||
|
||||
// Delete a few keys from the cache.
|
||||
for i := 0; i < len(accs); i += 2 {
|
||||
cache.delete(wantAccounts[i])
|
||||
}
|
||||
cache.delete(accounts.Account{Address: common.HexToAddress("fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e"), URL: "something"})
|
||||
|
||||
// Check content again after deletion.
|
||||
wantAccountsAfterDelete := []accounts.Account{
|
||||
wantAccounts[1],
|
||||
wantAccounts[3],
|
||||
wantAccounts[5],
|
||||
}
|
||||
list = cache.accounts()
|
||||
if !reflect.DeepEqual(list, wantAccountsAfterDelete) {
|
||||
t.Fatalf("got accounts after delete: %s\nwant %s", spew.Sdump(list), spew.Sdump(wantAccountsAfterDelete))
|
||||
}
|
||||
for _, a := range wantAccountsAfterDelete {
|
||||
if !cache.hasAddress(a.Address) {
|
||||
t.Errorf("expected hasAccount(%x) to return true", a.Address)
|
||||
}
|
||||
}
|
||||
if cache.hasAddress(wantAccounts[0].Address) {
|
||||
t.Errorf("expected hasAccount(%x) to return false", wantAccounts[0].Address)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCacheFind(t *testing.T) {
|
||||
dir := filepath.Join("testdata", "dir")
|
||||
cache := newAddrCache(dir)
|
||||
cache.watcher.running = true // prevent unexpected reloads
|
||||
|
||||
accs := []accounts.Account{
|
||||
{
|
||||
Address: common.HexToAddress("095e7baea6a6c7c4c2dfeb977efac326af552d87"),
|
||||
URL: filepath.Join(dir, "a.key"),
|
||||
},
|
||||
{
|
||||
Address: common.HexToAddress("2cac1adea150210703ba75ed097ddfe24e14f213"),
|
||||
URL: filepath.Join(dir, "b.key"),
|
||||
},
|
||||
{
|
||||
Address: common.HexToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"),
|
||||
URL: filepath.Join(dir, "c.key"),
|
||||
},
|
||||
{
|
||||
Address: common.HexToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"),
|
||||
URL: filepath.Join(dir, "c2.key"),
|
||||
},
|
||||
}
|
||||
for _, a := range accs {
|
||||
cache.add(a)
|
||||
}
|
||||
|
||||
nomatchAccount := accounts.Account{
|
||||
Address: common.HexToAddress("f466859ead1932d743d622cb74fc058882e8648a"),
|
||||
URL: filepath.Join(dir, "something"),
|
||||
}
|
||||
tests := []struct {
|
||||
Query accounts.Account
|
||||
WantResult accounts.Account
|
||||
WantError error
|
||||
}{
|
||||
// by address
|
||||
{Query: accounts.Account{Address: accs[0].Address}, WantResult: accs[0]},
|
||||
// by file
|
||||
{Query: accounts.Account{URL: accs[0].URL}, WantResult: accs[0]},
|
||||
// by basename
|
||||
{Query: accounts.Account{URL: filepath.Base(accs[0].URL)}, WantResult: accs[0]},
|
||||
// by file and address
|
||||
{Query: accs[0], WantResult: accs[0]},
|
||||
// ambiguous address, tie resolved by file
|
||||
{Query: accs[2], WantResult: accs[2]},
|
||||
// ambiguous address error
|
||||
{
|
||||
Query: accounts.Account{Address: accs[2].Address},
|
||||
WantError: &AmbiguousAddrError{
|
||||
Addr: accs[2].Address,
|
||||
Matches: []accounts.Account{accs[2], accs[3]},
|
||||
},
|
||||
},
|
||||
// no match error
|
||||
{Query: nomatchAccount, WantError: ErrNoMatch},
|
||||
{Query: accounts.Account{URL: nomatchAccount.URL}, WantError: ErrNoMatch},
|
||||
{Query: accounts.Account{URL: filepath.Base(nomatchAccount.URL)}, WantError: ErrNoMatch},
|
||||
{Query: accounts.Account{Address: nomatchAccount.Address}, WantError: ErrNoMatch},
|
||||
}
|
||||
for i, test := range tests {
|
||||
a, err := cache.find(test.Query)
|
||||
if !reflect.DeepEqual(err, test.WantError) {
|
||||
t.Errorf("test %d: error mismatch for query %v\ngot %q\nwant %q", i, test.Query, err, test.WantError)
|
||||
continue
|
||||
}
|
||||
if a != test.WantResult {
|
||||
t.Errorf("test %d: result mismatch for query %v\ngot %v\nwant %v", i, test.Query, a, test.WantResult)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
230
accounts/keystore/key.go
Normal file
230
accounts/keystore/key.go
Normal file
@ -0,0 +1,230 @@
|
||||
// Copyright 2014 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package keystore
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ecdsa"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/crypto/secp256k1"
|
||||
"github.com/pborman/uuid"
|
||||
)
|
||||
|
||||
const (
|
||||
version = 3
|
||||
)
|
||||
|
||||
type Key struct {
|
||||
Id uuid.UUID // Version 4 "random" for unique id not derived from key data
|
||||
// to simplify lookups we also store the address
|
||||
Address common.Address
|
||||
// we only store privkey as pubkey/address can be derived from it
|
||||
// privkey in this struct is always in plaintext
|
||||
PrivateKey *ecdsa.PrivateKey
|
||||
}
|
||||
|
||||
type keyStore interface {
|
||||
// Loads and decrypts the key from disk.
|
||||
GetKey(addr common.Address, filename string, auth string) (*Key, error)
|
||||
// Writes and encrypts the key.
|
||||
StoreKey(filename string, k *Key, auth string) error
|
||||
// Joins filename with the key directory unless it is already absolute.
|
||||
JoinPath(filename string) string
|
||||
}
|
||||
|
||||
type plainKeyJSON struct {
|
||||
Address string `json:"address"`
|
||||
PrivateKey string `json:"privatekey"`
|
||||
Id string `json:"id"`
|
||||
Version int `json:"version"`
|
||||
}
|
||||
|
||||
type encryptedKeyJSONV3 struct {
|
||||
Address string `json:"address"`
|
||||
Crypto cryptoJSON `json:"crypto"`
|
||||
Id string `json:"id"`
|
||||
Version int `json:"version"`
|
||||
}
|
||||
|
||||
type encryptedKeyJSONV1 struct {
|
||||
Address string `json:"address"`
|
||||
Crypto cryptoJSON `json:"crypto"`
|
||||
Id string `json:"id"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
type cryptoJSON struct {
|
||||
Cipher string `json:"cipher"`
|
||||
CipherText string `json:"ciphertext"`
|
||||
CipherParams cipherparamsJSON `json:"cipherparams"`
|
||||
KDF string `json:"kdf"`
|
||||
KDFParams map[string]interface{} `json:"kdfparams"`
|
||||
MAC string `json:"mac"`
|
||||
}
|
||||
|
||||
type cipherparamsJSON struct {
|
||||
IV string `json:"iv"`
|
||||
}
|
||||
|
||||
type scryptParamsJSON struct {
|
||||
N int `json:"n"`
|
||||
R int `json:"r"`
|
||||
P int `json:"p"`
|
||||
DkLen int `json:"dklen"`
|
||||
Salt string `json:"salt"`
|
||||
}
|
||||
|
||||
func (k *Key) MarshalJSON() (j []byte, err error) {
|
||||
jStruct := plainKeyJSON{
|
||||
hex.EncodeToString(k.Address[:]),
|
||||
hex.EncodeToString(crypto.FromECDSA(k.PrivateKey)),
|
||||
k.Id.String(),
|
||||
version,
|
||||
}
|
||||
j, err = json.Marshal(jStruct)
|
||||
return j, err
|
||||
}
|
||||
|
||||
func (k *Key) UnmarshalJSON(j []byte) (err error) {
|
||||
keyJSON := new(plainKeyJSON)
|
||||
err = json.Unmarshal(j, &keyJSON)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u := new(uuid.UUID)
|
||||
*u = uuid.Parse(keyJSON.Id)
|
||||
k.Id = *u
|
||||
addr, err := hex.DecodeString(keyJSON.Address)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
privkey, err := hex.DecodeString(keyJSON.PrivateKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
k.Address = common.BytesToAddress(addr)
|
||||
k.PrivateKey = crypto.ToECDSA(privkey)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func newKeyFromECDSA(privateKeyECDSA *ecdsa.PrivateKey) *Key {
|
||||
id := uuid.NewRandom()
|
||||
key := &Key{
|
||||
Id: id,
|
||||
Address: crypto.PubkeyToAddress(privateKeyECDSA.PublicKey),
|
||||
PrivateKey: privateKeyECDSA,
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
// NewKeyForDirectICAP generates a key whose address fits into < 155 bits so it can fit
|
||||
// into the Direct ICAP spec. for simplicity and easier compatibility with other libs, we
|
||||
// retry until the first byte is 0.
|
||||
func NewKeyForDirectICAP(rand io.Reader) *Key {
|
||||
randBytes := make([]byte, 64)
|
||||
_, err := rand.Read(randBytes)
|
||||
if err != nil {
|
||||
panic("key generation: could not read from random source: " + err.Error())
|
||||
}
|
||||
reader := bytes.NewReader(randBytes)
|
||||
privateKeyECDSA, err := ecdsa.GenerateKey(secp256k1.S256(), reader)
|
||||
if err != nil {
|
||||
panic("key generation: ecdsa.GenerateKey failed: " + err.Error())
|
||||
}
|
||||
key := newKeyFromECDSA(privateKeyECDSA)
|
||||
if !strings.HasPrefix(key.Address.Hex(), "0x00") {
|
||||
return NewKeyForDirectICAP(rand)
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
func newKey(rand io.Reader) (*Key, error) {
|
||||
privateKeyECDSA, err := ecdsa.GenerateKey(secp256k1.S256(), rand)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newKeyFromECDSA(privateKeyECDSA), nil
|
||||
}
|
||||
|
||||
func storeNewKey(ks keyStore, rand io.Reader, auth string) (*Key, accounts.Account, error) {
|
||||
key, err := newKey(rand)
|
||||
if err != nil {
|
||||
return nil, accounts.Account{}, err
|
||||
}
|
||||
a := accounts.Account{Address: key.Address, URL: ks.JoinPath(keyFileName(key.Address))}
|
||||
if err := ks.StoreKey(a.URL, key, auth); err != nil {
|
||||
zeroKey(key.PrivateKey)
|
||||
return nil, a, err
|
||||
}
|
||||
return key, a, err
|
||||
}
|
||||
|
||||
func writeKeyFile(file string, content []byte) error {
|
||||
// Create the keystore directory with appropriate permissions
|
||||
// in case it is not present yet.
|
||||
const dirPerm = 0700
|
||||
if err := os.MkdirAll(filepath.Dir(file), dirPerm); err != nil {
|
||||
return err
|
||||
}
|
||||
// Atomic write: create a temporary hidden file first
|
||||
// then move it into place. TempFile assigns mode 0600.
|
||||
f, err := ioutil.TempFile(filepath.Dir(file), "."+filepath.Base(file)+".tmp")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := f.Write(content); err != nil {
|
||||
f.Close()
|
||||
os.Remove(f.Name())
|
||||
return err
|
||||
}
|
||||
f.Close()
|
||||
return os.Rename(f.Name(), file)
|
||||
}
|
||||
|
||||
// keyFileName implements the naming convention for keyfiles:
|
||||
// UTC--<created_at UTC ISO8601>-<address hex>
|
||||
func keyFileName(keyAddr common.Address) string {
|
||||
ts := time.Now().UTC()
|
||||
return fmt.Sprintf("UTC--%s--%s", toISO8601(ts), hex.EncodeToString(keyAddr[:]))
|
||||
}
|
||||
|
||||
func toISO8601(t time.Time) string {
|
||||
var tz string
|
||||
name, offset := t.Zone()
|
||||
if name == "UTC" {
|
||||
tz = "Z"
|
||||
} else {
|
||||
tz = fmt.Sprintf("%03d00", offset/3600)
|
||||
}
|
||||
return fmt.Sprintf("%04d-%02d-%02dT%02d-%02d-%02d.%09d%s", t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), tz)
|
||||
}
|
362
accounts/keystore/keystore.go
Normal file
362
accounts/keystore/keystore.go
Normal file
@ -0,0 +1,362 @@
|
||||
// Copyright 2015 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// Package keystore implements encrypted storage of secp256k1 private keys.
|
||||
//
|
||||
// Keys are stored as encrypted JSON files according to the Web3 Secret Storage specification.
|
||||
// See https://github.com/ethereum/wiki/wiki/Web3-Secret-Storage-Definition for more information.
|
||||
package keystore
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
crand "crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNeedPasswordOrUnlock = accounts.NewAuthNeededError("password or unlock")
|
||||
ErrNoMatch = errors.New("no key for given address or file")
|
||||
ErrDecrypt = errors.New("could not decrypt key with given passphrase")
|
||||
)
|
||||
|
||||
// BackendType can be used to query the account manager for encrypted keystores.
|
||||
var BackendType = reflect.TypeOf(new(KeyStore))
|
||||
|
||||
// KeyStore manages a key storage directory on disk.
|
||||
type KeyStore struct {
|
||||
cache *addressCache
|
||||
keyStore keyStore
|
||||
mu sync.RWMutex
|
||||
unlocked map[common.Address]*unlocked
|
||||
}
|
||||
|
||||
type unlocked struct {
|
||||
*Key
|
||||
abort chan struct{}
|
||||
}
|
||||
|
||||
// NewKeyStore creates a keystore for the given directory.
|
||||
func NewKeyStore(keydir string, scryptN, scryptP int) *KeyStore {
|
||||
keydir, _ = filepath.Abs(keydir)
|
||||
ks := &KeyStore{keyStore: &keyStorePassphrase{keydir, scryptN, scryptP}}
|
||||
ks.init(keydir)
|
||||
return ks
|
||||
}
|
||||
|
||||
// NewPlaintextKeyStore creates a keystore for the given directory.
|
||||
// Deprecated: Use NewKeyStore.
|
||||
func NewPlaintextKeyStore(keydir string) *KeyStore {
|
||||
keydir, _ = filepath.Abs(keydir)
|
||||
ks := &KeyStore{keyStore: &keyStorePlain{keydir}}
|
||||
ks.init(keydir)
|
||||
return ks
|
||||
}
|
||||
|
||||
func (ks *KeyStore) init(keydir string) {
|
||||
ks.unlocked = make(map[common.Address]*unlocked)
|
||||
ks.cache = newAddrCache(keydir)
|
||||
// TODO: In order for this finalizer to work, there must be no references
|
||||
// to ks. addressCache doesn't keep a reference but unlocked keys do,
|
||||
// so the finalizer will not trigger until all timed unlocks have expired.
|
||||
runtime.SetFinalizer(ks, func(m *KeyStore) {
|
||||
m.cache.close()
|
||||
})
|
||||
}
|
||||
|
||||
// HasAddress reports whether a key with the given address is present.
|
||||
func (ks *KeyStore) HasAddress(addr common.Address) bool {
|
||||
return ks.cache.hasAddress(addr)
|
||||
}
|
||||
|
||||
// Accounts returns all key files present in the directory.
|
||||
func (ks *KeyStore) Accounts() []accounts.Account {
|
||||
return ks.cache.accounts()
|
||||
}
|
||||
|
||||
// Delete deletes the key matched by account if the passphrase is correct.
|
||||
// If the account contains no filename, the address must match a unique key.
|
||||
func (ks *KeyStore) Delete(a accounts.Account, passphrase string) error {
|
||||
// Decrypting the key isn't really necessary, but we do
|
||||
// it anyway to check the password and zero out the key
|
||||
// immediately afterwards.
|
||||
a, key, err := ks.getDecryptedKey(a, passphrase)
|
||||
if key != nil {
|
||||
zeroKey(key.PrivateKey)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// The order is crucial here. The key is dropped from the
|
||||
// cache after the file is gone so that a reload happening in
|
||||
// between won't insert it into the cache again.
|
||||
err = os.Remove(a.URL)
|
||||
if err == nil {
|
||||
ks.cache.delete(a)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// SignHash calculates a ECDSA signature for the given hash. The produced
|
||||
// signature is in the [R || S || V] format where V is 0 or 1.
|
||||
func (ks *KeyStore) SignHash(a accounts.Account, hash []byte) ([]byte, error) {
|
||||
// Look up the key to sign with and abort if it cannot be found
|
||||
ks.mu.RLock()
|
||||
defer ks.mu.RUnlock()
|
||||
|
||||
unlockedKey, found := ks.unlocked[a.Address]
|
||||
if !found {
|
||||
return nil, ErrNeedPasswordOrUnlock
|
||||
}
|
||||
// Sign the hash using plain ECDSA operations
|
||||
return crypto.Sign(hash, unlockedKey.PrivateKey)
|
||||
}
|
||||
|
||||
// SignTx signs the given transaction with the requested account.
|
||||
func (ks *KeyStore) SignTx(a accounts.Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) {
|
||||
// Look up the key to sign with and abort if it cannot be found
|
||||
ks.mu.RLock()
|
||||
defer ks.mu.RUnlock()
|
||||
|
||||
unlockedKey, found := ks.unlocked[a.Address]
|
||||
if !found {
|
||||
return nil, ErrNeedPasswordOrUnlock
|
||||
}
|
||||
// Depending on the presence of the chain ID, sign with EIP155 or homestead
|
||||
if chainID != nil {
|
||||
return types.SignTx(tx, types.NewEIP155Signer(chainID), unlockedKey.PrivateKey)
|
||||
}
|
||||
return types.SignTx(tx, types.HomesteadSigner{}, unlockedKey.PrivateKey)
|
||||
}
|
||||
|
||||
// SignHashWithPassphrase signs hash if the private key matching the given address
|
||||
// can be decrypted with the given passphrase. The produced signature is in the
|
||||
// [R || S || V] format where V is 0 or 1.
|
||||
func (ks *KeyStore) SignHashWithPassphrase(a accounts.Account, passphrase string, hash []byte) (signature []byte, err error) {
|
||||
_, key, err := ks.getDecryptedKey(a, passphrase)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer zeroKey(key.PrivateKey)
|
||||
return crypto.Sign(hash, key.PrivateKey)
|
||||
}
|
||||
|
||||
// SignTxWithPassphrase signs the transaction if the private key matching the
|
||||
// given address can be decrypted with the given passphrase.
|
||||
func (ks *KeyStore) SignTxWithPassphrase(a accounts.Account, passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) {
|
||||
_, key, err := ks.getDecryptedKey(a, passphrase)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer zeroKey(key.PrivateKey)
|
||||
|
||||
// Depending on the presence of the chain ID, sign with EIP155 or homestead
|
||||
if chainID != nil {
|
||||
return types.SignTx(tx, types.NewEIP155Signer(chainID), key.PrivateKey)
|
||||
}
|
||||
return types.SignTx(tx, types.HomesteadSigner{}, key.PrivateKey)
|
||||
}
|
||||
|
||||
// Unlock unlocks the given account indefinitely.
|
||||
func (ks *KeyStore) Unlock(a accounts.Account, passphrase string) error {
|
||||
return ks.TimedUnlock(a, passphrase, 0)
|
||||
}
|
||||
|
||||
// Lock removes the private key with the given address from memory.
|
||||
func (ks *KeyStore) Lock(addr common.Address) error {
|
||||
ks.mu.Lock()
|
||||
if unl, found := ks.unlocked[addr]; found {
|
||||
ks.mu.Unlock()
|
||||
ks.expire(addr, unl, time.Duration(0)*time.Nanosecond)
|
||||
} else {
|
||||
ks.mu.Unlock()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TimedUnlock unlocks the given account with the passphrase. The account
|
||||
// stays unlocked for the duration of timeout. A timeout of 0 unlocks the account
|
||||
// until the program exits. The account must match a unique key file.
|
||||
//
|
||||
// If the account address is already unlocked for a duration, TimedUnlock extends or
|
||||
// shortens the active unlock timeout. If the address was previously unlocked
|
||||
// indefinitely the timeout is not altered.
|
||||
func (ks *KeyStore) TimedUnlock(a accounts.Account, passphrase string, timeout time.Duration) error {
|
||||
a, key, err := ks.getDecryptedKey(a, passphrase)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ks.mu.Lock()
|
||||
defer ks.mu.Unlock()
|
||||
u, found := ks.unlocked[a.Address]
|
||||
if found {
|
||||
if u.abort == nil {
|
||||
// The address was unlocked indefinitely, so unlocking
|
||||
// it with a timeout would be confusing.
|
||||
zeroKey(key.PrivateKey)
|
||||
return nil
|
||||
} else {
|
||||
// Terminate the expire goroutine and replace it below.
|
||||
close(u.abort)
|
||||
}
|
||||
}
|
||||
if timeout > 0 {
|
||||
u = &unlocked{Key: key, abort: make(chan struct{})}
|
||||
go ks.expire(a.Address, u, timeout)
|
||||
} else {
|
||||
u = &unlocked{Key: key}
|
||||
}
|
||||
ks.unlocked[a.Address] = u
|
||||
return nil
|
||||
}
|
||||
|
||||
// Find resolves the given account into a unique entry in the keystore.
|
||||
func (ks *KeyStore) Find(a accounts.Account) (accounts.Account, error) {
|
||||
ks.cache.maybeReload()
|
||||
ks.cache.mu.Lock()
|
||||
a, err := ks.cache.find(a)
|
||||
ks.cache.mu.Unlock()
|
||||
return a, err
|
||||
}
|
||||
|
||||
func (ks *KeyStore) getDecryptedKey(a accounts.Account, auth string) (accounts.Account, *Key, error) {
|
||||
a, err := ks.Find(a)
|
||||
if err != nil {
|
||||
return a, nil, err
|
||||
}
|
||||
key, err := ks.keyStore.GetKey(a.Address, a.URL, auth)
|
||||
return a, key, err
|
||||
}
|
||||
|
||||
func (ks *KeyStore) expire(addr common.Address, u *unlocked, timeout time.Duration) {
|
||||
t := time.NewTimer(timeout)
|
||||
defer t.Stop()
|
||||
select {
|
||||
case <-u.abort:
|
||||
// just quit
|
||||
case <-t.C:
|
||||
ks.mu.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 ks.unlocked[addr] == u {
|
||||
zeroKey(u.PrivateKey)
|
||||
delete(ks.unlocked, addr)
|
||||
}
|
||||
ks.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// NewAccount generates a new key and stores it into the key directory,
|
||||
// encrypting it with the passphrase.
|
||||
func (ks *KeyStore) NewAccount(passphrase string) (accounts.Account, error) {
|
||||
_, account, err := storeNewKey(ks.keyStore, crand.Reader, passphrase)
|
||||
if err != nil {
|
||||
return accounts.Account{}, err
|
||||
}
|
||||
// Add the account to the cache immediately rather
|
||||
// than waiting for file system notifications to pick it up.
|
||||
ks.cache.add(account)
|
||||
return account, nil
|
||||
}
|
||||
|
||||
// Export exports as a JSON key, encrypted with newPassphrase.
|
||||
func (ks *KeyStore) Export(a accounts.Account, passphrase, newPassphrase string) (keyJSON []byte, err error) {
|
||||
_, key, err := ks.getDecryptedKey(a, passphrase)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var N, P int
|
||||
if store, ok := ks.keyStore.(*keyStorePassphrase); ok {
|
||||
N, P = store.scryptN, store.scryptP
|
||||
} else {
|
||||
N, P = StandardScryptN, StandardScryptP
|
||||
}
|
||||
return EncryptKey(key, newPassphrase, N, P)
|
||||
}
|
||||
|
||||
// Import stores the given encrypted JSON key into the key directory.
|
||||
func (ks *KeyStore) Import(keyJSON []byte, passphrase, newPassphrase string) (accounts.Account, error) {
|
||||
key, err := DecryptKey(keyJSON, passphrase)
|
||||
if key != nil && key.PrivateKey != nil {
|
||||
defer zeroKey(key.PrivateKey)
|
||||
}
|
||||
if err != nil {
|
||||
return accounts.Account{}, err
|
||||
}
|
||||
return ks.importKey(key, newPassphrase)
|
||||
}
|
||||
|
||||
// ImportECDSA stores the given key into the key directory, encrypting it with the passphrase.
|
||||
func (ks *KeyStore) ImportECDSA(priv *ecdsa.PrivateKey, passphrase string) (accounts.Account, error) {
|
||||
key := newKeyFromECDSA(priv)
|
||||
if ks.cache.hasAddress(key.Address) {
|
||||
return accounts.Account{}, fmt.Errorf("account already exists")
|
||||
}
|
||||
|
||||
return ks.importKey(key, passphrase)
|
||||
}
|
||||
|
||||
func (ks *KeyStore) importKey(key *Key, passphrase string) (accounts.Account, error) {
|
||||
a := accounts.Account{Address: key.Address, URL: ks.keyStore.JoinPath(keyFileName(key.Address))}
|
||||
if err := ks.keyStore.StoreKey(a.URL, key, passphrase); err != nil {
|
||||
return accounts.Account{}, err
|
||||
}
|
||||
ks.cache.add(a)
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// Update changes the passphrase of an existing account.
|
||||
func (ks *KeyStore) Update(a accounts.Account, passphrase, newPassphrase string) error {
|
||||
a, key, err := ks.getDecryptedKey(a, passphrase)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ks.keyStore.StoreKey(a.URL, key, newPassphrase)
|
||||
}
|
||||
|
||||
// ImportPreSaleKey decrypts the given Ethereum presale wallet and stores
|
||||
// a key file in the key directory. The key file is encrypted with the same passphrase.
|
||||
func (ks *KeyStore) ImportPreSaleKey(keyJSON []byte, passphrase string) (accounts.Account, error) {
|
||||
a, _, err := importPreSaleKey(ks.keyStore, keyJSON, passphrase)
|
||||
if err != nil {
|
||||
return a, err
|
||||
}
|
||||
ks.cache.add(a)
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// zeroKey zeroes a private key in memory.
|
||||
func zeroKey(k *ecdsa.PrivateKey) {
|
||||
b := k.D.Bits()
|
||||
for i := range b {
|
||||
b[i] = 0
|
||||
}
|
||||
}
|
305
accounts/keystore/keystore_passphrase.go
Normal file
305
accounts/keystore/keystore_passphrase.go
Normal file
@ -0,0 +1,305 @@
|
||||
// Copyright 2014 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/*
|
||||
|
||||
This key store behaves as KeyStorePlain with the difference that
|
||||
the private key is encrypted and on disk uses another JSON encoding.
|
||||
|
||||
The crypto is documented at https://github.com/ethereum/wiki/wiki/Web3-Secret-Storage-Definition
|
||||
|
||||
*/
|
||||
|
||||
package keystore
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/crypto/randentropy"
|
||||
"github.com/pborman/uuid"
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
"golang.org/x/crypto/scrypt"
|
||||
)
|
||||
|
||||
const (
|
||||
keyHeaderKDF = "scrypt"
|
||||
|
||||
// StandardScryptN is the N parameter of Scrypt encryption algorithm, using 256MB
|
||||
// memory and taking approximately 1s CPU time on a modern processor.
|
||||
StandardScryptN = 1 << 18
|
||||
|
||||
// StandardScryptP is the P parameter of Scrypt encryption algorithm, using 256MB
|
||||
// memory and taking approximately 1s CPU time on a modern processor.
|
||||
StandardScryptP = 1
|
||||
|
||||
// LightScryptN is the N parameter of Scrypt encryption algorithm, using 4MB
|
||||
// memory and taking approximately 100ms CPU time on a modern processor.
|
||||
LightScryptN = 1 << 12
|
||||
|
||||
// LightScryptP is the P parameter of Scrypt encryption algorithm, using 4MB
|
||||
// memory and taking approximately 100ms CPU time on a modern processor.
|
||||
LightScryptP = 6
|
||||
|
||||
scryptR = 8
|
||||
scryptDKLen = 32
|
||||
)
|
||||
|
||||
type keyStorePassphrase struct {
|
||||
keysDirPath string
|
||||
scryptN int
|
||||
scryptP int
|
||||
}
|
||||
|
||||
func (ks keyStorePassphrase) GetKey(addr common.Address, filename, auth string) (*Key, error) {
|
||||
// Load the key from the keystore and decrypt its contents
|
||||
keyjson, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key, err := DecryptKey(keyjson, auth)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Make sure we're really operating on the requested key (no swap attacks)
|
||||
if key.Address != addr {
|
||||
return nil, fmt.Errorf("key content mismatch: have account %x, want %x", key.Address, addr)
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func (ks keyStorePassphrase) StoreKey(filename string, key *Key, auth string) error {
|
||||
keyjson, err := EncryptKey(key, auth, ks.scryptN, ks.scryptP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return writeKeyFile(filename, keyjson)
|
||||
}
|
||||
|
||||
func (ks keyStorePassphrase) JoinPath(filename string) string {
|
||||
if filepath.IsAbs(filename) {
|
||||
return filename
|
||||
} else {
|
||||
return filepath.Join(ks.keysDirPath, filename)
|
||||
}
|
||||
}
|
||||
|
||||
// EncryptKey encrypts a key using the specified scrypt parameters into a json
|
||||
// blob that can be decrypted later on.
|
||||
func EncryptKey(key *Key, auth string, scryptN, scryptP int) ([]byte, error) {
|
||||
authArray := []byte(auth)
|
||||
salt := randentropy.GetEntropyCSPRNG(32)
|
||||
derivedKey, err := scrypt.Key(authArray, salt, scryptN, scryptR, scryptP, scryptDKLen)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
encryptKey := derivedKey[:16]
|
||||
keyBytes0 := crypto.FromECDSA(key.PrivateKey)
|
||||
keyBytes := common.LeftPadBytes(keyBytes0, 32)
|
||||
|
||||
iv := randentropy.GetEntropyCSPRNG(aes.BlockSize) // 16
|
||||
cipherText, err := aesCTRXOR(encryptKey, keyBytes, iv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mac := crypto.Keccak256(derivedKey[16:32], cipherText)
|
||||
|
||||
scryptParamsJSON := make(map[string]interface{}, 5)
|
||||
scryptParamsJSON["n"] = scryptN
|
||||
scryptParamsJSON["r"] = scryptR
|
||||
scryptParamsJSON["p"] = scryptP
|
||||
scryptParamsJSON["dklen"] = scryptDKLen
|
||||
scryptParamsJSON["salt"] = hex.EncodeToString(salt)
|
||||
|
||||
cipherParamsJSON := cipherparamsJSON{
|
||||
IV: hex.EncodeToString(iv),
|
||||
}
|
||||
|
||||
cryptoStruct := cryptoJSON{
|
||||
Cipher: "aes-128-ctr",
|
||||
CipherText: hex.EncodeToString(cipherText),
|
||||
CipherParams: cipherParamsJSON,
|
||||
KDF: "scrypt",
|
||||
KDFParams: scryptParamsJSON,
|
||||
MAC: hex.EncodeToString(mac),
|
||||
}
|
||||
encryptedKeyJSONV3 := encryptedKeyJSONV3{
|
||||
hex.EncodeToString(key.Address[:]),
|
||||
cryptoStruct,
|
||||
key.Id.String(),
|
||||
version,
|
||||
}
|
||||
return json.Marshal(encryptedKeyJSONV3)
|
||||
}
|
||||
|
||||
// DecryptKey decrypts a key from a json blob, returning the private key itself.
|
||||
func DecryptKey(keyjson []byte, auth string) (*Key, error) {
|
||||
// Parse the json into a simple map to fetch the key version
|
||||
m := make(map[string]interface{})
|
||||
if err := json.Unmarshal(keyjson, &m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Depending on the version try to parse one way or another
|
||||
var (
|
||||
keyBytes, keyId []byte
|
||||
err error
|
||||
)
|
||||
if version, ok := m["version"].(string); ok && version == "1" {
|
||||
k := new(encryptedKeyJSONV1)
|
||||
if err := json.Unmarshal(keyjson, k); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keyBytes, keyId, err = decryptKeyV1(k, auth)
|
||||
} else {
|
||||
k := new(encryptedKeyJSONV3)
|
||||
if err := json.Unmarshal(keyjson, k); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keyBytes, keyId, err = decryptKeyV3(k, auth)
|
||||
}
|
||||
// Handle any decryption errors and return the key
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key := crypto.ToECDSA(keyBytes)
|
||||
return &Key{
|
||||
Id: uuid.UUID(keyId),
|
||||
Address: crypto.PubkeyToAddress(key.PublicKey),
|
||||
PrivateKey: key,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func decryptKeyV3(keyProtected *encryptedKeyJSONV3, auth string) (keyBytes []byte, keyId []byte, err error) {
|
||||
if keyProtected.Version != version {
|
||||
return nil, nil, fmt.Errorf("Version not supported: %v", keyProtected.Version)
|
||||
}
|
||||
|
||||
if keyProtected.Crypto.Cipher != "aes-128-ctr" {
|
||||
return nil, nil, fmt.Errorf("Cipher not supported: %v", keyProtected.Crypto.Cipher)
|
||||
}
|
||||
|
||||
keyId = uuid.Parse(keyProtected.Id)
|
||||
mac, err := hex.DecodeString(keyProtected.Crypto.MAC)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
iv, err := hex.DecodeString(keyProtected.Crypto.CipherParams.IV)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
cipherText, err := hex.DecodeString(keyProtected.Crypto.CipherText)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
derivedKey, err := getKDFKey(keyProtected.Crypto, auth)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
calculatedMAC := crypto.Keccak256(derivedKey[16:32], cipherText)
|
||||
if !bytes.Equal(calculatedMAC, mac) {
|
||||
return nil, nil, ErrDecrypt
|
||||
}
|
||||
|
||||
plainText, err := aesCTRXOR(derivedKey[:16], cipherText, iv)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return plainText, keyId, err
|
||||
}
|
||||
|
||||
func decryptKeyV1(keyProtected *encryptedKeyJSONV1, auth string) (keyBytes []byte, keyId []byte, err error) {
|
||||
keyId = uuid.Parse(keyProtected.Id)
|
||||
mac, err := hex.DecodeString(keyProtected.Crypto.MAC)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
iv, err := hex.DecodeString(keyProtected.Crypto.CipherParams.IV)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
cipherText, err := hex.DecodeString(keyProtected.Crypto.CipherText)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
derivedKey, err := getKDFKey(keyProtected.Crypto, auth)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
calculatedMAC := crypto.Keccak256(derivedKey[16:32], cipherText)
|
||||
if !bytes.Equal(calculatedMAC, mac) {
|
||||
return nil, nil, ErrDecrypt
|
||||
}
|
||||
|
||||
plainText, err := aesCBCDecrypt(crypto.Keccak256(derivedKey[:16])[:16], cipherText, iv)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return plainText, keyId, err
|
||||
}
|
||||
|
||||
func getKDFKey(cryptoJSON cryptoJSON, auth string) ([]byte, error) {
|
||||
authArray := []byte(auth)
|
||||
salt, err := hex.DecodeString(cryptoJSON.KDFParams["salt"].(string))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dkLen := ensureInt(cryptoJSON.KDFParams["dklen"])
|
||||
|
||||
if cryptoJSON.KDF == "scrypt" {
|
||||
n := ensureInt(cryptoJSON.KDFParams["n"])
|
||||
r := ensureInt(cryptoJSON.KDFParams["r"])
|
||||
p := ensureInt(cryptoJSON.KDFParams["p"])
|
||||
return scrypt.Key(authArray, salt, n, r, p, dkLen)
|
||||
|
||||
} else if cryptoJSON.KDF == "pbkdf2" {
|
||||
c := ensureInt(cryptoJSON.KDFParams["c"])
|
||||
prf := cryptoJSON.KDFParams["prf"].(string)
|
||||
if prf != "hmac-sha256" {
|
||||
return nil, fmt.Errorf("Unsupported PBKDF2 PRF: %s", prf)
|
||||
}
|
||||
key := pbkdf2.Key(authArray, salt, c, dkLen, sha256.New)
|
||||
return key, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("Unsupported KDF: %s", cryptoJSON.KDF)
|
||||
}
|
||||
|
||||
// TODO: can we do without this when unmarshalling dynamic JSON?
|
||||
// why do integers in KDF params end up as float64 and not int after
|
||||
// unmarshal?
|
||||
func ensureInt(x interface{}) int {
|
||||
res, ok := x.(int)
|
||||
if !ok {
|
||||
res = int(x.(float64))
|
||||
}
|
||||
return res
|
||||
}
|
60
accounts/keystore/keystore_passphrase_test.go
Normal file
60
accounts/keystore/keystore_passphrase_test.go
Normal file
@ -0,0 +1,60 @@
|
||||
// Copyright 2016 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package keystore
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
)
|
||||
|
||||
const (
|
||||
veryLightScryptN = 2
|
||||
veryLightScryptP = 1
|
||||
)
|
||||
|
||||
// Tests that a json key file can be decrypted and encrypted in multiple rounds.
|
||||
func TestKeyEncryptDecrypt(t *testing.T) {
|
||||
keyjson, err := ioutil.ReadFile("testdata/very-light-scrypt.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
password := ""
|
||||
address := common.HexToAddress("45dea0fb0bba44f4fcf290bba71fd57d7117cbb8")
|
||||
|
||||
// Do a few rounds of decryption and encryption
|
||||
for i := 0; i < 3; i++ {
|
||||
// Try a bad password first
|
||||
if _, err := DecryptKey(keyjson, password+"bad"); err == nil {
|
||||
t.Errorf("test %d: json key decrypted with bad password", i)
|
||||
}
|
||||
// Decrypt with the correct password
|
||||
key, err := DecryptKey(keyjson, password)
|
||||
if err != nil {
|
||||
t.Errorf("test %d: json key failed to decrypt: %v", i, err)
|
||||
}
|
||||
if key.Address != address {
|
||||
t.Errorf("test %d: key address mismatch: have %x, want %x", i, key.Address, address)
|
||||
}
|
||||
// Recrypt with a new password and start over
|
||||
password += "new data appended"
|
||||
if keyjson, err = EncryptKey(key, password, veryLightScryptN, veryLightScryptP); err != nil {
|
||||
t.Errorf("test %d: failed to recrypt key %v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
62
accounts/keystore/keystore_plain.go
Normal file
62
accounts/keystore/keystore_plain.go
Normal file
@ -0,0 +1,62 @@
|
||||
// Copyright 2015 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package keystore
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
)
|
||||
|
||||
type keyStorePlain struct {
|
||||
keysDirPath string
|
||||
}
|
||||
|
||||
func (ks keyStorePlain) GetKey(addr common.Address, filename, auth string) (*Key, error) {
|
||||
fd, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer fd.Close()
|
||||
key := new(Key)
|
||||
if err := json.NewDecoder(fd).Decode(key); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if key.Address != addr {
|
||||
return nil, fmt.Errorf("key content mismatch: have address %x, want %x", key.Address, addr)
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func (ks keyStorePlain) StoreKey(filename string, key *Key, auth string) error {
|
||||
content, err := json.Marshal(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return writeKeyFile(filename, content)
|
||||
}
|
||||
|
||||
func (ks keyStorePlain) JoinPath(filename string) string {
|
||||
if filepath.IsAbs(filename) {
|
||||
return filename
|
||||
} else {
|
||||
return filepath.Join(ks.keysDirPath, filename)
|
||||
}
|
||||
}
|
253
accounts/keystore/keystore_plain_test.go
Normal file
253
accounts/keystore/keystore_plain_test.go
Normal file
@ -0,0 +1,253 @@
|
||||
// Copyright 2014 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package keystore
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
)
|
||||
|
||||
func tmpKeyStoreIface(t *testing.T, encrypted bool) (dir string, ks keyStore) {
|
||||
d, err := ioutil.TempDir("", "geth-keystore-test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if encrypted {
|
||||
ks = &keyStorePassphrase{d, veryLightScryptN, veryLightScryptP}
|
||||
} else {
|
||||
ks = &keyStorePlain{d}
|
||||
}
|
||||
return d, ks
|
||||
}
|
||||
|
||||
func TestKeyStorePlain(t *testing.T) {
|
||||
dir, ks := tmpKeyStoreIface(t, false)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
pass := "" // not used but required by API
|
||||
k1, account, err := storeNewKey(ks, rand.Reader, pass)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
k2, err := ks.GetKey(k1.Address, account.URL, pass)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(k1.Address, k2.Address) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(k1.PrivateKey, k2.PrivateKey) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyStorePassphrase(t *testing.T) {
|
||||
dir, ks := tmpKeyStoreIface(t, true)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
pass := "foo"
|
||||
k1, account, err := storeNewKey(ks, rand.Reader, pass)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
k2, err := ks.GetKey(k1.Address, account.URL, pass)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(k1.Address, k2.Address) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(k1.PrivateKey, k2.PrivateKey) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyStorePassphraseDecryptionFail(t *testing.T) {
|
||||
dir, ks := tmpKeyStoreIface(t, true)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
pass := "foo"
|
||||
k1, account, err := storeNewKey(ks, rand.Reader, pass)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err = ks.GetKey(k1.Address, account.URL, "bar"); err != ErrDecrypt {
|
||||
t.Fatalf("wrong error for invalid passphrase\ngot %q\nwant %q", err, ErrDecrypt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestImportPreSaleKey(t *testing.T) {
|
||||
dir, ks := tmpKeyStoreIface(t, true)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
// file content of a presale key file generated with:
|
||||
// python pyethsaletool.py genwallet
|
||||
// with password "foo"
|
||||
fileContent := "{\"encseed\": \"26d87f5f2bf9835f9a47eefae571bc09f9107bb13d54ff12a4ec095d01f83897494cf34f7bed2ed34126ecba9db7b62de56c9d7cd136520a0427bfb11b8954ba7ac39b90d4650d3448e31185affcd74226a68f1e94b1108e6e0a4a91cdd83eba\", \"ethaddr\": \"d4584b5f6229b7be90727b0fc8c6b91bb427821f\", \"email\": \"gustav.simonsson@gmail.com\", \"btcaddr\": \"1EVknXyFC68kKNLkh6YnKzW41svSRoaAcx\"}"
|
||||
pass := "foo"
|
||||
account, _, err := importPreSaleKey(ks, []byte(fileContent), pass)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if account.Address != common.HexToAddress("d4584b5f6229b7be90727b0fc8c6b91bb427821f") {
|
||||
t.Errorf("imported account has wrong address %x", account.Address)
|
||||
}
|
||||
if !strings.HasPrefix(account.URL, dir) {
|
||||
t.Errorf("imported account file not in keystore directory: %q", account.URL)
|
||||
}
|
||||
}
|
||||
|
||||
// Test and utils for the key store tests in the Ethereum JSON tests;
|
||||
// testdataKeyStoreTests/basic_tests.json
|
||||
type KeyStoreTestV3 struct {
|
||||
Json encryptedKeyJSONV3
|
||||
Password string
|
||||
Priv string
|
||||
}
|
||||
|
||||
type KeyStoreTestV1 struct {
|
||||
Json encryptedKeyJSONV1
|
||||
Password string
|
||||
Priv string
|
||||
}
|
||||
|
||||
func TestV3_PBKDF2_1(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := loadKeyStoreTestV3("testdata/v3_test_vector.json", t)
|
||||
testDecryptV3(tests["wikipage_test_vector_pbkdf2"], t)
|
||||
}
|
||||
|
||||
func TestV3_PBKDF2_2(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := loadKeyStoreTestV3("../../tests/files/KeyStoreTests/basic_tests.json", t)
|
||||
testDecryptV3(tests["test1"], t)
|
||||
}
|
||||
|
||||
func TestV3_PBKDF2_3(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := loadKeyStoreTestV3("../../tests/files/KeyStoreTests/basic_tests.json", t)
|
||||
testDecryptV3(tests["python_generated_test_with_odd_iv"], t)
|
||||
}
|
||||
|
||||
func TestV3_PBKDF2_4(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := loadKeyStoreTestV3("../../tests/files/KeyStoreTests/basic_tests.json", t)
|
||||
testDecryptV3(tests["evilnonce"], t)
|
||||
}
|
||||
|
||||
func TestV3_Scrypt_1(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := loadKeyStoreTestV3("testdata/v3_test_vector.json", t)
|
||||
testDecryptV3(tests["wikipage_test_vector_scrypt"], t)
|
||||
}
|
||||
|
||||
func TestV3_Scrypt_2(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := loadKeyStoreTestV3("../../tests/files/KeyStoreTests/basic_tests.json", t)
|
||||
testDecryptV3(tests["test2"], t)
|
||||
}
|
||||
|
||||
func TestV1_1(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := loadKeyStoreTestV1("testdata/v1_test_vector.json", t)
|
||||
testDecryptV1(tests["test1"], t)
|
||||
}
|
||||
|
||||
func TestV1_2(t *testing.T) {
|
||||
t.Parallel()
|
||||
ks := &keyStorePassphrase{"testdata/v1", LightScryptN, LightScryptP}
|
||||
addr := common.HexToAddress("cb61d5a9c4896fb9658090b597ef0e7be6f7b67e")
|
||||
file := "testdata/v1/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e"
|
||||
k, err := ks.GetKey(addr, file, "g")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
privHex := hex.EncodeToString(crypto.FromECDSA(k.PrivateKey))
|
||||
expectedHex := "d1b1178d3529626a1a93e073f65028370d14c7eb0936eb42abef05db6f37ad7d"
|
||||
if privHex != expectedHex {
|
||||
t.Fatal(fmt.Errorf("Unexpected privkey: %v, expected %v", privHex, expectedHex))
|
||||
}
|
||||
}
|
||||
|
||||
func testDecryptV3(test KeyStoreTestV3, t *testing.T) {
|
||||
privBytes, _, err := decryptKeyV3(&test.Json, test.Password)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
privHex := hex.EncodeToString(privBytes)
|
||||
if test.Priv != privHex {
|
||||
t.Fatal(fmt.Errorf("Decrypted bytes not equal to test, expected %v have %v", test.Priv, privHex))
|
||||
}
|
||||
}
|
||||
|
||||
func testDecryptV1(test KeyStoreTestV1, t *testing.T) {
|
||||
privBytes, _, err := decryptKeyV1(&test.Json, test.Password)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
privHex := hex.EncodeToString(privBytes)
|
||||
if test.Priv != privHex {
|
||||
t.Fatal(fmt.Errorf("Decrypted bytes not equal to test, expected %v have %v", test.Priv, privHex))
|
||||
}
|
||||
}
|
||||
|
||||
func loadKeyStoreTestV3(file string, t *testing.T) map[string]KeyStoreTestV3 {
|
||||
tests := make(map[string]KeyStoreTestV3)
|
||||
err := common.LoadJSON(file, &tests)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return tests
|
||||
}
|
||||
|
||||
func loadKeyStoreTestV1(file string, t *testing.T) map[string]KeyStoreTestV1 {
|
||||
tests := make(map[string]KeyStoreTestV1)
|
||||
err := common.LoadJSON(file, &tests)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return tests
|
||||
}
|
||||
|
||||
func TestKeyForDirectICAP(t *testing.T) {
|
||||
t.Parallel()
|
||||
key := NewKeyForDirectICAP(rand.Reader)
|
||||
if !strings.HasPrefix(key.Address.Hex(), "0x00") {
|
||||
t.Errorf("Expected first address byte to be zero, have: %s", key.Address.Hex())
|
||||
}
|
||||
}
|
||||
|
||||
func TestV3_31_Byte_Key(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := loadKeyStoreTestV3("testdata/v3_test_vector.json", t)
|
||||
testDecryptV3(tests["31_byte_key"], t)
|
||||
}
|
||||
|
||||
func TestV3_30_Byte_Key(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := loadKeyStoreTestV3("testdata/v3_test_vector.json", t)
|
||||
testDecryptV3(tests["30_byte_key"], t)
|
||||
}
|
225
accounts/keystore/keystore_test.go
Normal file
225
accounts/keystore/keystore_test.go
Normal file
@ -0,0 +1,225 @@
|
||||
// Copyright 2015 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package keystore
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
)
|
||||
|
||||
var testSigData = make([]byte, 32)
|
||||
|
||||
func TestKeyStore(t *testing.T) {
|
||||
dir, ks := tmpKeyStore(t, true)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
a, err := ks.NewAccount("foo")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.HasPrefix(a.URL, dir) {
|
||||
t.Errorf("account file %s doesn't have dir prefix", a.URL)
|
||||
}
|
||||
stat, err := os.Stat(a.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("account file %s doesn't exist (%v)", a.URL, err)
|
||||
}
|
||||
if runtime.GOOS != "windows" && stat.Mode() != 0600 {
|
||||
t.Fatalf("account file has wrong mode: got %o, want %o", stat.Mode(), 0600)
|
||||
}
|
||||
if !ks.HasAddress(a.Address) {
|
||||
t.Errorf("HasAccount(%x) should've returned true", a.Address)
|
||||
}
|
||||
if err := ks.Update(a, "foo", "bar"); err != nil {
|
||||
t.Errorf("Update error: %v", err)
|
||||
}
|
||||
if err := ks.Delete(a, "bar"); err != nil {
|
||||
t.Errorf("Delete error: %v", err)
|
||||
}
|
||||
if common.FileExist(a.URL) {
|
||||
t.Errorf("account file %s should be gone after Delete", a.URL)
|
||||
}
|
||||
if ks.HasAddress(a.Address) {
|
||||
t.Errorf("HasAccount(%x) should've returned true after Delete", a.Address)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSign(t *testing.T) {
|
||||
dir, ks := tmpKeyStore(t, true)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
pass := "" // not used but required by API
|
||||
a1, err := ks.NewAccount(pass)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := ks.Unlock(a1, ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := ks.SignHash(accounts.Account{Address: a1.Address}, testSigData); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignWithPassphrase(t *testing.T) {
|
||||
dir, ks := tmpKeyStore(t, true)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
pass := "passwd"
|
||||
acc, err := ks.NewAccount(pass)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, unlocked := ks.unlocked[acc.Address]; unlocked {
|
||||
t.Fatal("expected account to be locked")
|
||||
}
|
||||
|
||||
_, err = ks.SignHashWithPassphrase(acc, pass, testSigData)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, unlocked := ks.unlocked[acc.Address]; unlocked {
|
||||
t.Fatal("expected account to be locked")
|
||||
}
|
||||
|
||||
if _, err = ks.SignHashWithPassphrase(acc, "invalid passwd", testSigData); err == nil {
|
||||
t.Fatal("expected SignHashWithPassphrase to fail with invalid password")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTimedUnlock(t *testing.T) {
|
||||
dir, ks := tmpKeyStore(t, true)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
pass := "foo"
|
||||
a1, err := ks.NewAccount(pass)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Signing without passphrase fails because account is locked
|
||||
_, err = ks.SignHash(accounts.Account{Address: a1.Address}, testSigData)
|
||||
if err != ErrNeedPasswordOrUnlock {
|
||||
t.Fatal("Signing should've failed with ErrNeedPasswordOrUnlock before unlocking, got ", err)
|
||||
}
|
||||
|
||||
// Signing with passphrase works
|
||||
if err = ks.TimedUnlock(a1, pass, 100*time.Millisecond); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Signing without passphrase works because account is temp unlocked
|
||||
_, err = ks.SignHash(accounts.Account{Address: a1.Address}, testSigData)
|
||||
if err != nil {
|
||||
t.Fatal("Signing shouldn't return an error after unlocking, got ", err)
|
||||
}
|
||||
|
||||
// Signing fails again after automatic locking
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
_, err = ks.SignHash(accounts.Account{Address: a1.Address}, testSigData)
|
||||
if err != ErrNeedPasswordOrUnlock {
|
||||
t.Fatal("Signing should've failed with ErrNeedPasswordOrUnlock timeout expired, got ", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOverrideUnlock(t *testing.T) {
|
||||
dir, ks := tmpKeyStore(t, false)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
pass := "foo"
|
||||
a1, err := ks.NewAccount(pass)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Unlock indefinitely.
|
||||
if err = ks.TimedUnlock(a1, pass, 5*time.Minute); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Signing without passphrase works because account is temp unlocked
|
||||
_, err = ks.SignHash(accounts.Account{Address: a1.Address}, testSigData)
|
||||
if err != nil {
|
||||
t.Fatal("Signing shouldn't return an error after unlocking, got ", err)
|
||||
}
|
||||
|
||||
// reset unlock to a shorter period, invalidates the previous unlock
|
||||
if err = ks.TimedUnlock(a1, pass, 100*time.Millisecond); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Signing without passphrase still works because account is temp unlocked
|
||||
_, err = ks.SignHash(accounts.Account{Address: a1.Address}, testSigData)
|
||||
if err != nil {
|
||||
t.Fatal("Signing shouldn't return an error after unlocking, got ", err)
|
||||
}
|
||||
|
||||
// Signing fails again after automatic locking
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
_, err = ks.SignHash(accounts.Account{Address: a1.Address}, testSigData)
|
||||
if err != ErrNeedPasswordOrUnlock {
|
||||
t.Fatal("Signing should've failed with ErrNeedPasswordOrUnlock timeout expired, got ", err)
|
||||
}
|
||||
}
|
||||
|
||||
// This test should fail under -race if signing races the expiration goroutine.
|
||||
func TestSignRace(t *testing.T) {
|
||||
dir, ks := tmpKeyStore(t, false)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
// Create a test account.
|
||||
a1, err := ks.NewAccount("")
|
||||
if err != nil {
|
||||
t.Fatal("could not create the test account", err)
|
||||
}
|
||||
|
||||
if err := ks.TimedUnlock(a1, "", 15*time.Millisecond); err != nil {
|
||||
t.Fatal("could not unlock the test account", err)
|
||||
}
|
||||
end := time.Now().Add(500 * time.Millisecond)
|
||||
for time.Now().Before(end) {
|
||||
if _, err := ks.SignHash(accounts.Account{Address: a1.Address}, testSigData); err == ErrNeedPasswordOrUnlock {
|
||||
return
|
||||
} else if err != nil {
|
||||
t.Errorf("Sign error: %v", err)
|
||||
return
|
||||
}
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
}
|
||||
t.Errorf("Account did not lock within the timeout")
|
||||
}
|
||||
|
||||
func tmpKeyStore(t *testing.T, encrypted bool) (string, *KeyStore) {
|
||||
d, err := ioutil.TempDir("", "eth-keystore-test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
new := NewPlaintextKeyStore
|
||||
if encrypted {
|
||||
new = func(kd string) *KeyStore { return NewKeyStore(kd, veryLightScryptN, veryLightScryptP) }
|
||||
}
|
||||
return d, new(d)
|
||||
}
|
137
accounts/keystore/presale.go
Normal file
137
accounts/keystore/presale.go
Normal file
@ -0,0 +1,137 @@
|
||||
// Copyright 2016 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package keystore
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/pborman/uuid"
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
)
|
||||
|
||||
// creates a Key and stores that in the given KeyStore by decrypting a presale key JSON
|
||||
func importPreSaleKey(keyStore keyStore, keyJSON []byte, password string) (accounts.Account, *Key, error) {
|
||||
key, err := decryptPreSaleKey(keyJSON, password)
|
||||
if err != nil {
|
||||
return accounts.Account{}, nil, err
|
||||
}
|
||||
key.Id = uuid.NewRandom()
|
||||
a := accounts.Account{Address: key.Address, URL: keyStore.JoinPath(keyFileName(key.Address))}
|
||||
err = keyStore.StoreKey(a.URL, key, password)
|
||||
return a, key, err
|
||||
}
|
||||
|
||||
func decryptPreSaleKey(fileContent []byte, password string) (key *Key, err error) {
|
||||
preSaleKeyStruct := struct {
|
||||
EncSeed string
|
||||
EthAddr string
|
||||
Email string
|
||||
BtcAddr string
|
||||
}{}
|
||||
err = json.Unmarshal(fileContent, &preSaleKeyStruct)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
encSeedBytes, err := hex.DecodeString(preSaleKeyStruct.EncSeed)
|
||||
if err != nil {
|
||||
return nil, errors.New("invalid hex in encSeed")
|
||||
}
|
||||
iv := encSeedBytes[:16]
|
||||
cipherText := encSeedBytes[16:]
|
||||
/*
|
||||
See https://github.com/ethereum/pyethsaletool
|
||||
|
||||
pyethsaletool generates the encryption key from password by
|
||||
2000 rounds of PBKDF2 with HMAC-SHA-256 using password as salt (:().
|
||||
16 byte key length within PBKDF2 and resulting key is used as AES key
|
||||
*/
|
||||
passBytes := []byte(password)
|
||||
derivedKey := pbkdf2.Key(passBytes, passBytes, 2000, 16, sha256.New)
|
||||
plainText, err := aesCBCDecrypt(derivedKey, cipherText, iv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ethPriv := crypto.Keccak256(plainText)
|
||||
ecKey := crypto.ToECDSA(ethPriv)
|
||||
key = &Key{
|
||||
Id: nil,
|
||||
Address: crypto.PubkeyToAddress(ecKey.PublicKey),
|
||||
PrivateKey: ecKey,
|
||||
}
|
||||
derivedAddr := hex.EncodeToString(key.Address.Bytes()) // needed because .Hex() gives leading "0x"
|
||||
expectedAddr := preSaleKeyStruct.EthAddr
|
||||
if derivedAddr != expectedAddr {
|
||||
err = fmt.Errorf("decrypted addr '%s' not equal to expected addr '%s'", derivedAddr, expectedAddr)
|
||||
}
|
||||
return key, err
|
||||
}
|
||||
|
||||
func aesCTRXOR(key, inText, iv []byte) ([]byte, error) {
|
||||
// AES-128 is selected due to size of encryptKey.
|
||||
aesBlock, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stream := cipher.NewCTR(aesBlock, iv)
|
||||
outText := make([]byte, len(inText))
|
||||
stream.XORKeyStream(outText, inText)
|
||||
return outText, err
|
||||
}
|
||||
|
||||
func aesCBCDecrypt(key, cipherText, iv []byte) ([]byte, error) {
|
||||
aesBlock, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
decrypter := cipher.NewCBCDecrypter(aesBlock, iv)
|
||||
paddedPlaintext := make([]byte, len(cipherText))
|
||||
decrypter.CryptBlocks(paddedPlaintext, cipherText)
|
||||
plaintext := pkcs7Unpad(paddedPlaintext)
|
||||
if plaintext == nil {
|
||||
return nil, ErrDecrypt
|
||||
}
|
||||
return plaintext, err
|
||||
}
|
||||
|
||||
// From https://leanpub.com/gocrypto/read#leanpub-auto-block-cipher-modes
|
||||
func pkcs7Unpad(in []byte) []byte {
|
||||
if len(in) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
padding := in[len(in)-1]
|
||||
if int(padding) > len(in) || padding > aes.BlockSize {
|
||||
return nil
|
||||
} else if padding == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for i := len(in) - 1; i > len(in)-int(padding)-1; i-- {
|
||||
if in[i] != padding {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return in[:len(in)-int(padding)]
|
||||
}
|
1
accounts/keystore/testdata/dupes/1
vendored
Normal file
1
accounts/keystore/testdata/dupes/1
vendored
Normal file
@ -0,0 +1 @@
|
||||
{"address":"f466859ead1932d743d622cb74fc058882e8648a","crypto":{"cipher":"aes-128-ctr","ciphertext":"cb664472deacb41a2e995fa7f96fe29ce744471deb8d146a0e43c7898c9ddd4d","cipherparams":{"iv":"dfd9ee70812add5f4b8f89d0811c9158"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":8,"p":16,"r":8,"salt":"0d6769bf016d45c479213990d6a08d938469c4adad8a02ce507b4a4e7b7739f1"},"mac":"bac9af994b15a45dd39669fc66f9aa8a3b9dd8c22cb16e4d8d7ea089d0f1a1a9"},"id":"472e8b3d-afb6-45b5-8111-72c89895099a","version":3}
|
1
accounts/keystore/testdata/dupes/2
vendored
Normal file
1
accounts/keystore/testdata/dupes/2
vendored
Normal file
@ -0,0 +1 @@
|
||||
{"address":"f466859ead1932d743d622cb74fc058882e8648a","crypto":{"cipher":"aes-128-ctr","ciphertext":"cb664472deacb41a2e995fa7f96fe29ce744471deb8d146a0e43c7898c9ddd4d","cipherparams":{"iv":"dfd9ee70812add5f4b8f89d0811c9158"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":8,"p":16,"r":8,"salt":"0d6769bf016d45c479213990d6a08d938469c4adad8a02ce507b4a4e7b7739f1"},"mac":"bac9af994b15a45dd39669fc66f9aa8a3b9dd8c22cb16e4d8d7ea089d0f1a1a9"},"id":"472e8b3d-afb6-45b5-8111-72c89895099a","version":3}
|
1
accounts/keystore/testdata/dupes/foo
vendored
Normal file
1
accounts/keystore/testdata/dupes/foo
vendored
Normal file
@ -0,0 +1 @@
|
||||
{"address":"7ef5a6135f1fd6a02593eedc869c6d41d934aef8","crypto":{"cipher":"aes-128-ctr","ciphertext":"1d0839166e7a15b9c1333fc865d69858b22df26815ccf601b28219b6192974e1","cipherparams":{"iv":"8df6caa7ff1b00c4e871f002cb7921ed"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":8,"p":16,"r":8,"salt":"e5e6ef3f4ea695f496b643ebd3f75c0aa58ef4070e90c80c5d3fb0241bf1595c"},"mac":"6d16dfde774845e4585357f24bce530528bc69f4f84e1e22880d34fa45c273e5"},"id":"950077c7-71e3-4c44-a4a1-143919141ed4","version":3}
|
1
accounts/keystore/testdata/keystore/.hiddenfile
vendored
Normal file
1
accounts/keystore/testdata/keystore/.hiddenfile
vendored
Normal file
@ -0,0 +1 @@
|
||||
{"address":"f466859ead1932d743d622cb74fc058882e8648a","crypto":{"cipher":"aes-128-ctr","ciphertext":"cb664472deacb41a2e995fa7f96fe29ce744471deb8d146a0e43c7898c9ddd4d","cipherparams":{"iv":"dfd9ee70812add5f4b8f89d0811c9158"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":8,"p":16,"r":8,"salt":"0d6769bf016d45c479213990d6a08d938469c4adad8a02ce507b4a4e7b7739f1"},"mac":"bac9af994b15a45dd39669fc66f9aa8a3b9dd8c22cb16e4d8d7ea089d0f1a1a9"},"id":"472e8b3d-afb6-45b5-8111-72c89895099a","version":3}
|
21
accounts/keystore/testdata/keystore/README
vendored
Normal file
21
accounts/keystore/testdata/keystore/README
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
This directory contains accounts for testing.
|
||||
The passphrase that unlocks them is "foobar".
|
||||
|
||||
The "good" key files which are supposed to be loadable are:
|
||||
|
||||
- File: UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8
|
||||
Address: 0x7ef5a6135f1fd6a02593eedc869c6d41d934aef8
|
||||
- File: aaa
|
||||
Address: 0xf466859ead1932d743d622cb74fc058882e8648a
|
||||
- File: zzz
|
||||
Address: 0x289d485d9771714cce91d3393d764e1311907acc
|
||||
|
||||
The other files (including this README) are broken in various ways
|
||||
and should not be picked up by package accounts:
|
||||
|
||||
- File: no-address (missing address field, otherwise same as "aaa")
|
||||
- File: garbage (file with random data)
|
||||
- File: empty (file with no content)
|
||||
- File: swapfile~ (should be skipped)
|
||||
- File: .hiddenfile (should be skipped)
|
||||
- File: foo/... (should be skipped because it is a directory)
|
@ -0,0 +1 @@
|
||||
{"address":"7ef5a6135f1fd6a02593eedc869c6d41d934aef8","crypto":{"cipher":"aes-128-ctr","ciphertext":"1d0839166e7a15b9c1333fc865d69858b22df26815ccf601b28219b6192974e1","cipherparams":{"iv":"8df6caa7ff1b00c4e871f002cb7921ed"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":8,"p":16,"r":8,"salt":"e5e6ef3f4ea695f496b643ebd3f75c0aa58ef4070e90c80c5d3fb0241bf1595c"},"mac":"6d16dfde774845e4585357f24bce530528bc69f4f84e1e22880d34fa45c273e5"},"id":"950077c7-71e3-4c44-a4a1-143919141ed4","version":3}
|
1
accounts/keystore/testdata/keystore/aaa
vendored
Normal file
1
accounts/keystore/testdata/keystore/aaa
vendored
Normal file
@ -0,0 +1 @@
|
||||
{"address":"f466859ead1932d743d622cb74fc058882e8648a","crypto":{"cipher":"aes-128-ctr","ciphertext":"cb664472deacb41a2e995fa7f96fe29ce744471deb8d146a0e43c7898c9ddd4d","cipherparams":{"iv":"dfd9ee70812add5f4b8f89d0811c9158"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":8,"p":16,"r":8,"salt":"0d6769bf016d45c479213990d6a08d938469c4adad8a02ce507b4a4e7b7739f1"},"mac":"bac9af994b15a45dd39669fc66f9aa8a3b9dd8c22cb16e4d8d7ea089d0f1a1a9"},"id":"472e8b3d-afb6-45b5-8111-72c89895099a","version":3}
|
0
accounts/keystore/testdata/keystore/empty
vendored
Normal file
0
accounts/keystore/testdata/keystore/empty
vendored
Normal file
1
accounts/keystore/testdata/keystore/foo/fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e
vendored
Normal file
1
accounts/keystore/testdata/keystore/foo/fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e
vendored
Normal file
@ -0,0 +1 @@
|
||||
{"address":"fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e","crypto":{"cipher":"aes-128-ctr","ciphertext":"8124d5134aa4a927c79fd852989e4b5419397566f04b0936a1eb1d168c7c68a5","cipherparams":{"iv":"e2febe17176414dd2cda28287947eb2f"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":4096,"p":6,"r":8,"salt":"44b415ede89f3bdd6830390a21b78965f571b347a589d1d943029f016c5e8bd5"},"mac":"5e149ff25bfd9dd45746a84bb2bcd2f015f2cbca2b6d25c5de8c29617f71fe5b"},"id":"d6ac5452-2b2c-4d3c-ad80-4bf0327d971c","version":3}
|
BIN
accounts/keystore/testdata/keystore/garbage
vendored
Normal file
BIN
accounts/keystore/testdata/keystore/garbage
vendored
Normal file
Binary file not shown.
1
accounts/keystore/testdata/keystore/no-address
vendored
Normal file
1
accounts/keystore/testdata/keystore/no-address
vendored
Normal file
@ -0,0 +1 @@
|
||||
{"crypto":{"cipher":"aes-128-ctr","ciphertext":"cb664472deacb41a2e995fa7f96fe29ce744471deb8d146a0e43c7898c9ddd4d","cipherparams":{"iv":"dfd9ee70812add5f4b8f89d0811c9158"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":8,"p":16,"r":8,"salt":"0d6769bf016d45c479213990d6a08d938469c4adad8a02ce507b4a4e7b7739f1"},"mac":"bac9af994b15a45dd39669fc66f9aa8a3b9dd8c22cb16e4d8d7ea089d0f1a1a9"},"id":"472e8b3d-afb6-45b5-8111-72c89895099a","version":3}
|
1
accounts/keystore/testdata/keystore/zero
vendored
Normal file
1
accounts/keystore/testdata/keystore/zero
vendored
Normal file
@ -0,0 +1 @@
|
||||
{"address":"0000000000000000000000000000000000000000","crypto":{"cipher":"aes-128-ctr","ciphertext":"cb664472deacb41a2e995fa7f96fe29ce744471deb8d146a0e43c7898c9ddd4d","cipherparams":{"iv":"dfd9ee70812add5f4b8f89d0811c9158"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":8,"p":16,"r":8,"salt":"0d6769bf016d45c479213990d6a08d938469c4adad8a02ce507b4a4e7b7739f1"},"mac":"bac9af994b15a45dd39669fc66f9aa8a3b9dd8c22cb16e4d8d7ea089d0f1a1a9"},"id":"472e8b3d-afb6-45b5-8111-72c89895099a","version":3}
|
1
accounts/keystore/testdata/keystore/zzz
vendored
Normal file
1
accounts/keystore/testdata/keystore/zzz
vendored
Normal file
@ -0,0 +1 @@
|
||||
{"address":"289d485d9771714cce91d3393d764e1311907acc","crypto":{"cipher":"aes-128-ctr","ciphertext":"faf32ca89d286b107f5e6d842802e05263c49b78d46eac74e6109e9a963378ab","cipherparams":{"iv":"558833eec4a665a8c55608d7d503407d"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":8,"p":16,"r":8,"salt":"d571fff447ffb24314f9513f5160246f09997b857ac71348b73e785aab40dc04"},"mac":"21edb85ff7d0dab1767b9bf498f2c3cb7be7609490756bd32300bb213b59effe"},"id":"3279afcf-55ba-43ff-8997-02dcc46a6525","version":3}
|
@ -0,0 +1 @@
|
||||
{"address":"cb61d5a9c4896fb9658090b597ef0e7be6f7b67e","Crypto":{"cipher":"aes-128-cbc","ciphertext":"6143d3192db8b66eabd693d9c4e414dcfaee52abda451af79ccf474dafb35f1bfc7ea013aa9d2ee35969a1a2e8d752d0","cipherparams":{"iv":"35337770fc2117994ecdcad026bccff4"},"kdf":"scrypt","kdfparams":{"n":262144,"r":8,"p":1,"dklen":32,"salt":"9afcddebca541253a2f4053391c673ff9fe23097cd8555d149d929e4ccf1257f"},"mac":"3f3d5af884b17a100b0b3232c0636c230a54dc2ac8d986227219b0dd89197644","version":"1"},"id":"e25f7c1f-d318-4f29-b62c-687190d4d299","version":"1"}
|
28
accounts/keystore/testdata/v1_test_vector.json
vendored
Normal file
28
accounts/keystore/testdata/v1_test_vector.json
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"test1": {
|
||||
"json": {
|
||||
"Crypto": {
|
||||
"cipher": "aes-128-cbc",
|
||||
"cipherparams": {
|
||||
"iv": "35337770fc2117994ecdcad026bccff4"
|
||||
},
|
||||
"ciphertext": "6143d3192db8b66eabd693d9c4e414dcfaee52abda451af79ccf474dafb35f1bfc7ea013aa9d2ee35969a1a2e8d752d0",
|
||||
"kdf": "scrypt",
|
||||
"kdfparams": {
|
||||
"dklen": 32,
|
||||
"n": 262144,
|
||||
"p": 1,
|
||||
"r": 8,
|
||||
"salt": "9afcddebca541253a2f4053391c673ff9fe23097cd8555d149d929e4ccf1257f"
|
||||
},
|
||||
"mac": "3f3d5af884b17a100b0b3232c0636c230a54dc2ac8d986227219b0dd89197644",
|
||||
"version": "1"
|
||||
},
|
||||
"address": "cb61d5a9c4896fb9658090b597ef0e7be6f7b67e",
|
||||
"id": "e25f7c1f-d318-4f29-b62c-687190d4d299",
|
||||
"version": "1"
|
||||
},
|
||||
"password": "g",
|
||||
"priv": "d1b1178d3529626a1a93e073f65028370d14c7eb0936eb42abef05db6f37ad7d"
|
||||
}
|
||||
}
|
97
accounts/keystore/testdata/v3_test_vector.json
vendored
Normal file
97
accounts/keystore/testdata/v3_test_vector.json
vendored
Normal file
@ -0,0 +1,97 @@
|
||||
{
|
||||
"wikipage_test_vector_scrypt": {
|
||||
"json": {
|
||||
"crypto" : {
|
||||
"cipher" : "aes-128-ctr",
|
||||
"cipherparams" : {
|
||||
"iv" : "83dbcc02d8ccb40e466191a123791e0e"
|
||||
},
|
||||
"ciphertext" : "d172bf743a674da9cdad04534d56926ef8358534d458fffccd4e6ad2fbde479c",
|
||||
"kdf" : "scrypt",
|
||||
"kdfparams" : {
|
||||
"dklen" : 32,
|
||||
"n" : 262144,
|
||||
"r" : 1,
|
||||
"p" : 8,
|
||||
"salt" : "ab0c7876052600dd703518d6fc3fe8984592145b591fc8fb5c6d43190334ba19"
|
||||
},
|
||||
"mac" : "2103ac29920d71da29f15d75b4a16dbe95cfd7ff8faea1056c33131d846e3097"
|
||||
},
|
||||
"id" : "3198bc9c-6672-5ab3-d995-4942343ae5b6",
|
||||
"version" : 3
|
||||
},
|
||||
"password": "testpassword",
|
||||
"priv": "7a28b5ba57c53603b0b07b56bba752f7784bf506fa95edc395f5cf6c7514fe9d"
|
||||
},
|
||||
"wikipage_test_vector_pbkdf2": {
|
||||
"json": {
|
||||
"crypto" : {
|
||||
"cipher" : "aes-128-ctr",
|
||||
"cipherparams" : {
|
||||
"iv" : "6087dab2f9fdbbfaddc31a909735c1e6"
|
||||
},
|
||||
"ciphertext" : "5318b4d5bcd28de64ee5559e671353e16f075ecae9f99c7a79a38af5f869aa46",
|
||||
"kdf" : "pbkdf2",
|
||||
"kdfparams" : {
|
||||
"c" : 262144,
|
||||
"dklen" : 32,
|
||||
"prf" : "hmac-sha256",
|
||||
"salt" : "ae3cd4e7013836a3df6bd7241b12db061dbe2c6785853cce422d148a624ce0bd"
|
||||
},
|
||||
"mac" : "517ead924a9d0dc3124507e3393d175ce3ff7c1e96529c6c555ce9e51205e9b2"
|
||||
},
|
||||
"id" : "3198bc9c-6672-5ab3-d995-4942343ae5b6",
|
||||
"version" : 3
|
||||
},
|
||||
"password": "testpassword",
|
||||
"priv": "7a28b5ba57c53603b0b07b56bba752f7784bf506fa95edc395f5cf6c7514fe9d"
|
||||
},
|
||||
"31_byte_key": {
|
||||
"json": {
|
||||
"crypto" : {
|
||||
"cipher" : "aes-128-ctr",
|
||||
"cipherparams" : {
|
||||
"iv" : "e0c41130a323adc1446fc82f724bca2f"
|
||||
},
|
||||
"ciphertext" : "9517cd5bdbe69076f9bf5057248c6c050141e970efa36ce53692d5d59a3984",
|
||||
"kdf" : "scrypt",
|
||||
"kdfparams" : {
|
||||
"dklen" : 32,
|
||||
"n" : 2,
|
||||
"r" : 8,
|
||||
"p" : 1,
|
||||
"salt" : "711f816911c92d649fb4c84b047915679933555030b3552c1212609b38208c63"
|
||||
},
|
||||
"mac" : "d5e116151c6aa71470e67a7d42c9620c75c4d23229847dcc127794f0732b0db5"
|
||||
},
|
||||
"id" : "fecfc4ce-e956-48fd-953b-30f8b52ed66c",
|
||||
"version" : 3
|
||||
},
|
||||
"password": "foo",
|
||||
"priv": "fa7b3db73dc7dfdf8c5fbdb796d741e4488628c41fc4febd9160a866ba0f35"
|
||||
},
|
||||
"30_byte_key": {
|
||||
"json": {
|
||||
"crypto" : {
|
||||
"cipher" : "aes-128-ctr",
|
||||
"cipherparams" : {
|
||||
"iv" : "3ca92af36ad7c2cd92454c59cea5ef00"
|
||||
},
|
||||
"ciphertext" : "108b7d34f3442fc26ab1ab90ca91476ba6bfa8c00975a49ef9051dc675aa",
|
||||
"kdf" : "scrypt",
|
||||
"kdfparams" : {
|
||||
"dklen" : 32,
|
||||
"n" : 2,
|
||||
"r" : 8,
|
||||
"p" : 1,
|
||||
"salt" : "d0769e608fb86cda848065642a9c6fa046845c928175662b8e356c77f914cd3b"
|
||||
},
|
||||
"mac" : "75d0e6759f7b3cefa319c3be41680ab6beea7d8328653474bd06706d4cc67420"
|
||||
},
|
||||
"id" : "a37e1559-5955-450d-8075-7b8931b392b2",
|
||||
"version" : 3
|
||||
},
|
||||
"password": "foo",
|
||||
"priv": "81c29e8142bb6a81bef5a92bda7a8328a5c85bb2f9542e76f9b0f94fc018"
|
||||
}
|
||||
}
|
1
accounts/keystore/testdata/very-light-scrypt.json
vendored
Normal file
1
accounts/keystore/testdata/very-light-scrypt.json
vendored
Normal file
@ -0,0 +1 @@
|
||||
{"address":"45dea0fb0bba44f4fcf290bba71fd57d7117cbb8","crypto":{"cipher":"aes-128-ctr","ciphertext":"b87781948a1befd247bff51ef4063f716cf6c2d3481163e9a8f42e1f9bb74145","cipherparams":{"iv":"dc4926b48a105133d2f16b96833abf1e"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":2,"p":1,"r":8,"salt":"004244bbdc51cadda545b1cfa43cff9ed2ae88e08c61f1479dbb45410722f8f0"},"mac":"39990c1684557447940d4c69e06b1b82b2aceacb43f284df65c956daf3046b85"},"id":"ce541d8d-c79b-40f8-9f8c-20f59616faba","version":3}
|
113
accounts/keystore/watch.go
Normal file
113
accounts/keystore/watch.go
Normal file
@ -0,0 +1,113 @@
|
||||
// Copyright 2016 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// +build darwin,!ios freebsd linux,!arm64 netbsd solaris
|
||||
|
||||
package keystore
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/logger"
|
||||
"github.com/ethereum/go-ethereum/logger/glog"
|
||||
"github.com/rjeczalik/notify"
|
||||
)
|
||||
|
||||
type watcher struct {
|
||||
ac *addressCache
|
||||
starting bool
|
||||
running bool
|
||||
ev chan notify.EventInfo
|
||||
quit chan struct{}
|
||||
}
|
||||
|
||||
func newWatcher(ac *addressCache) *watcher {
|
||||
return &watcher{
|
||||
ac: ac,
|
||||
ev: make(chan notify.EventInfo, 10),
|
||||
quit: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// starts the watcher loop in the background.
|
||||
// Start a watcher in the background if that's not already in progress.
|
||||
// The caller must hold w.ac.mu.
|
||||
func (w *watcher) start() {
|
||||
if w.starting || w.running {
|
||||
return
|
||||
}
|
||||
w.starting = true
|
||||
go w.loop()
|
||||
}
|
||||
|
||||
func (w *watcher) close() {
|
||||
close(w.quit)
|
||||
}
|
||||
|
||||
func (w *watcher) loop() {
|
||||
defer func() {
|
||||
w.ac.mu.Lock()
|
||||
w.running = false
|
||||
w.starting = false
|
||||
w.ac.mu.Unlock()
|
||||
}()
|
||||
|
||||
err := notify.Watch(w.ac.keydir, w.ev, notify.All)
|
||||
if err != nil {
|
||||
glog.V(logger.Detail).Infof("can't watch %s: %v", w.ac.keydir, err)
|
||||
return
|
||||
}
|
||||
defer notify.Stop(w.ev)
|
||||
glog.V(logger.Detail).Infof("now watching %s", w.ac.keydir)
|
||||
defer glog.V(logger.Detail).Infof("no longer watching %s", w.ac.keydir)
|
||||
|
||||
w.ac.mu.Lock()
|
||||
w.running = true
|
||||
w.ac.mu.Unlock()
|
||||
|
||||
// Wait for file system events and reload.
|
||||
// When an event occurs, the reload call is delayed a bit so that
|
||||
// multiple events arriving quickly only cause a single reload.
|
||||
var (
|
||||
debounce = time.NewTimer(0)
|
||||
debounceDuration = 500 * time.Millisecond
|
||||
inCycle, hadEvent bool
|
||||
)
|
||||
defer debounce.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-w.quit:
|
||||
return
|
||||
case <-w.ev:
|
||||
if !inCycle {
|
||||
debounce.Reset(debounceDuration)
|
||||
inCycle = true
|
||||
} else {
|
||||
hadEvent = true
|
||||
}
|
||||
case <-debounce.C:
|
||||
w.ac.mu.Lock()
|
||||
w.ac.reload()
|
||||
w.ac.mu.Unlock()
|
||||
if hadEvent {
|
||||
debounce.Reset(debounceDuration)
|
||||
inCycle, hadEvent = true, false
|
||||
} else {
|
||||
inCycle, hadEvent = false, false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
28
accounts/keystore/watch_fallback.go
Normal file
28
accounts/keystore/watch_fallback.go
Normal file
@ -0,0 +1,28 @@
|
||||
// Copyright 2016 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// +build ios linux,arm64 windows !darwin,!freebsd,!linux,!netbsd,!solaris
|
||||
|
||||
// This is the fallback implementation of directory watching.
|
||||
// It is used on unsupported platforms.
|
||||
|
||||
package keystore
|
||||
|
||||
type watcher struct{ running bool }
|
||||
|
||||
func newWatcher(*addressCache) *watcher { return new(watcher) }
|
||||
func (*watcher) start() {}
|
||||
func (*watcher) close() {}
|
Reference in New Issue
Block a user