accounts/scwallet: ordered wallets, tighter events, derivation logs
This commit is contained in:
parent
114de0fe2a
commit
386943943f
@ -36,14 +36,12 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ebfe/scard"
|
"github.com/ebfe/scard"
|
||||||
"github.com/ethereum/go-ethereum/accounts"
|
"github.com/ethereum/go-ethereum/accounts"
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
|
||||||
"github.com/ethereum/go-ethereum/event"
|
"github.com/ethereum/go-ethereum/event"
|
||||||
"github.com/ethereum/go-ethereum/log"
|
"github.com/ethereum/go-ethereum/log"
|
||||||
)
|
)
|
||||||
@ -71,9 +69,10 @@ type smartcardPairing struct {
|
|||||||
type Hub struct {
|
type Hub struct {
|
||||||
scheme string // Protocol scheme prefixing account and wallet URLs.
|
scheme string // Protocol scheme prefixing account and wallet URLs.
|
||||||
|
|
||||||
context *scard.Context
|
context *scard.Context
|
||||||
datadir string
|
datadir string
|
||||||
pairings map[string]smartcardPairing
|
pairings map[string]smartcardPairing
|
||||||
|
|
||||||
refreshed time.Time // Time instance when the list of wallets was last refreshed
|
refreshed time.Time // Time instance when the list of wallets was last refreshed
|
||||||
wallets map[string]*Wallet // Mapping from reader names to wallet instances
|
wallets map[string]*Wallet // Mapping from reader names to wallet instances
|
||||||
updateFeed event.Feed // Event feed to notify wallet additions/removals
|
updateFeed event.Feed // Event feed to notify wallet additions/removals
|
||||||
@ -82,11 +81,9 @@ type Hub struct {
|
|||||||
|
|
||||||
quit chan chan error
|
quit chan chan error
|
||||||
|
|
||||||
stateLock sync.Mutex // Protects the internals of the hub from racey access
|
stateLock sync.RWMutex // Protects the internals of the hub from racey access
|
||||||
}
|
}
|
||||||
|
|
||||||
var HubType = reflect.TypeOf(&Hub{})
|
|
||||||
|
|
||||||
func (hub *Hub) readPairings() error {
|
func (hub *Hub) readPairings() error {
|
||||||
hub.pairings = make(map[string]smartcardPairing)
|
hub.pairings = make(map[string]smartcardPairing)
|
||||||
pairingFile, err := os.Open(hub.datadir + "/smartcards.json")
|
pairingFile, err := os.Open(hub.datadir + "/smartcards.json")
|
||||||
@ -158,7 +155,6 @@ func NewHub(scheme string, datadir string) (*Hub, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
hub := &Hub{
|
hub := &Hub{
|
||||||
scheme: scheme,
|
scheme: scheme,
|
||||||
context: context,
|
context: context,
|
||||||
@ -166,28 +162,31 @@ func NewHub(scheme string, datadir string) (*Hub, error) {
|
|||||||
wallets: make(map[string]*Wallet),
|
wallets: make(map[string]*Wallet),
|
||||||
quit: make(chan chan error),
|
quit: make(chan chan error),
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := hub.readPairings(); err != nil {
|
if err := hub.readPairings(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
hub.refreshWallets()
|
hub.refreshWallets()
|
||||||
return hub, nil
|
return hub, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wallets implements accounts.Backend, returning all the currently tracked
|
// Wallets implements accounts.Backend, returning all the currently tracked smart
|
||||||
// devices that appear to be hardware wallets.
|
// cards that appear to be hardware wallets.
|
||||||
func (hub *Hub) Wallets() []accounts.Wallet {
|
func (hub *Hub) Wallets() []accounts.Wallet {
|
||||||
// Make sure the list of wallets is up to date
|
// Make sure the list of wallets is up to date
|
||||||
hub.stateLock.Lock()
|
|
||||||
defer hub.stateLock.Unlock()
|
|
||||||
|
|
||||||
hub.refreshWallets()
|
hub.refreshWallets()
|
||||||
|
|
||||||
|
hub.stateLock.RLock()
|
||||||
|
defer hub.stateLock.RUnlock()
|
||||||
|
|
||||||
cpy := make([]accounts.Wallet, 0, len(hub.wallets))
|
cpy := make([]accounts.Wallet, 0, len(hub.wallets))
|
||||||
for _, wallet := range hub.wallets {
|
for _, wallet := range hub.wallets {
|
||||||
if wallet != nil {
|
cpy = append(cpy, wallet)
|
||||||
cpy = append(cpy, wallet)
|
}
|
||||||
|
for i := 0; i < len(cpy); i++ {
|
||||||
|
for j := i + 1; j < len(cpy); j++ {
|
||||||
|
if cpy[i].URL().Cmp(cpy[j].URL()) > 0 {
|
||||||
|
cpy[i], cpy[j] = cpy[j], cpy[i]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return cpy
|
return cpy
|
||||||
@ -196,67 +195,77 @@ func (hub *Hub) Wallets() []accounts.Wallet {
|
|||||||
// refreshWallets scans the devices attached to the machine and updates the
|
// refreshWallets scans the devices attached to the machine and updates the
|
||||||
// list of wallets based on the found devices.
|
// list of wallets based on the found devices.
|
||||||
func (hub *Hub) refreshWallets() {
|
func (hub *Hub) refreshWallets() {
|
||||||
|
// Don't scan the USB like crazy it the user fetches wallets in a loop
|
||||||
|
hub.stateLock.RLock()
|
||||||
elapsed := time.Since(hub.refreshed)
|
elapsed := time.Since(hub.refreshed)
|
||||||
|
hub.stateLock.RUnlock()
|
||||||
|
|
||||||
if elapsed < refreshThrottling {
|
if elapsed < refreshThrottling {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// Retrieve all the smart card reader to check for cards
|
||||||
readers, err := hub.context.ListReaders()
|
readers, err := hub.context.ListReaders()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Error listing readers", "err", err)
|
// This is a perverted hack, the scard library returns an error if no card
|
||||||
|
// readers are present instead of simply returning an empty list. We don't
|
||||||
|
// want to fill the user's log with errors, so filter those out.
|
||||||
|
if err.Error() != "scard: Cannot find a smart card reader." {
|
||||||
|
log.Error("Failed to enumerate smart card readers", "err", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// Transform the current list of wallets into the new one
|
||||||
|
hub.stateLock.Lock()
|
||||||
|
|
||||||
events := []accounts.WalletEvent{}
|
events := []accounts.WalletEvent{}
|
||||||
seen := make(map[string]struct{})
|
seen := make(map[string]struct{})
|
||||||
|
|
||||||
for _, reader := range readers {
|
for _, reader := range readers {
|
||||||
if wallet, ok := hub.wallets[reader]; ok {
|
// Mark the reader as present
|
||||||
// We already know about this card; check it's still present
|
|
||||||
if err := wallet.ping(); err != nil {
|
|
||||||
log.Debug("Got error pinging wallet", "reader", reader, "err", err)
|
|
||||||
} else {
|
|
||||||
seen[reader] = struct{}{}
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seen[reader] = struct{}{}
|
seen[reader] = struct{}{}
|
||||||
|
|
||||||
|
// If we alreay know about this card, skip to the next reader, otherwise clean up
|
||||||
|
if wallet, ok := hub.wallets[reader]; ok {
|
||||||
|
if err := wallet.ping(); err == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
wallet.Close()
|
||||||
|
events = append(events, accounts.WalletEvent{Wallet: wallet, Kind: accounts.WalletDropped})
|
||||||
|
delete(hub.wallets, reader)
|
||||||
|
}
|
||||||
|
// New card detected, try to connect to it
|
||||||
card, err := hub.context.Connect(reader, scard.ShareShared, scard.ProtocolAny)
|
card, err := hub.context.Connect(reader, scard.ShareShared, scard.ProtocolAny)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug("Error opening card", "reader", reader, "err", err)
|
log.Debug("Failed to open smart card", "reader", reader, "err", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
wallet := NewWallet(hub, card)
|
wallet := NewWallet(hub, card)
|
||||||
err = wallet.connect()
|
if err = wallet.connect(); err != nil {
|
||||||
if err != nil {
|
log.Debug("Failed to connect to smart card", "reader", reader, "err", err)
|
||||||
log.Debug("Error connecting to wallet", "reader", reader, "err", err)
|
|
||||||
card.Disconnect(scard.LeaveCard)
|
card.Disconnect(scard.LeaveCard)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
// Card connected, start tracking in amongs the wallets
|
||||||
hub.wallets[reader] = wallet
|
hub.wallets[reader] = wallet
|
||||||
events = append(events, accounts.WalletEvent{Wallet: wallet, Kind: accounts.WalletArrived})
|
events = append(events, accounts.WalletEvent{Wallet: wallet, Kind: accounts.WalletArrived})
|
||||||
log.Info("Found new smartcard wallet", "reader", reader, "publicKey", hexutil.Encode(wallet.PublicKey[:4]))
|
|
||||||
}
|
}
|
||||||
|
// Remove any wallets no longer present
|
||||||
// Remove any wallets we no longer see
|
for reader, wallet := range hub.wallets {
|
||||||
for k, wallet := range hub.wallets {
|
if _, ok := seen[reader]; !ok {
|
||||||
if _, ok := seen[k]; !ok {
|
|
||||||
log.Info("Wallet disconnected", "pubkey", hexutil.Encode(wallet.PublicKey[:4]), "reader", k)
|
|
||||||
wallet.Close()
|
wallet.Close()
|
||||||
events = append(events, accounts.WalletEvent{Wallet: wallet, Kind: accounts.WalletDropped})
|
events = append(events, accounts.WalletEvent{Wallet: wallet, Kind: accounts.WalletDropped})
|
||||||
delete(hub.wallets, k)
|
delete(hub.wallets, reader)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
hub.refreshed = time.Now()
|
||||||
|
hub.stateLock.Unlock()
|
||||||
|
|
||||||
for _, event := range events {
|
for _, event := range events {
|
||||||
hub.updateFeed.Send(event)
|
hub.updateFeed.Send(event)
|
||||||
}
|
}
|
||||||
hub.refreshed = time.Now()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subscribe implements accounts.Backend, creating an async subscription to
|
// Subscribe implements accounts.Backend, creating an async subscription to
|
||||||
// receive notifications on the addition or removal of wallets.
|
// receive notifications on the addition or removal of smart card wallets.
|
||||||
func (hub *Hub) Subscribe(sink chan<- accounts.WalletEvent) event.Subscription {
|
func (hub *Hub) Subscribe(sink chan<- accounts.WalletEvent) event.Subscription {
|
||||||
// We need the mutex to reliably start/stop the update loop
|
// We need the mutex to reliably start/stop the update loop
|
||||||
hub.stateLock.Lock()
|
hub.stateLock.Lock()
|
||||||
@ -274,16 +283,18 @@ func (hub *Hub) Subscribe(sink chan<- accounts.WalletEvent) event.Subscription {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// updater is responsible for maintaining an up-to-date list of wallets managed
|
// updater is responsible for maintaining an up-to-date list of wallets managed
|
||||||
// by the hub, and for firing wallet addition/removal events.
|
// by the smart card hub, and for firing wallet addition/removal events.
|
||||||
func (hub *Hub) updater() {
|
func (hub *Hub) updater() {
|
||||||
for {
|
for {
|
||||||
|
// TODO: Wait for a USB hotplug event (not supported yet) or a refresh timeout
|
||||||
|
// <-hub.changes
|
||||||
time.Sleep(refreshCycle)
|
time.Sleep(refreshCycle)
|
||||||
|
|
||||||
// Run the wallet refresher
|
// Run the wallet refresher
|
||||||
hub.stateLock.Lock()
|
|
||||||
hub.refreshWallets()
|
hub.refreshWallets()
|
||||||
|
|
||||||
// If all our subscribers left, stop the updater
|
// If all our subscribers left, stop the updater
|
||||||
|
hub.stateLock.Lock()
|
||||||
if hub.updateScope.Count() == 0 {
|
if hub.updateScope.Count() == 0 {
|
||||||
hub.updating = false
|
hub.updating = false
|
||||||
hub.stateLock.Unlock()
|
hub.stateLock.Unlock()
|
||||||
|
@ -93,10 +93,11 @@ type Wallet struct {
|
|||||||
Hub *Hub // A handle to the Hub that instantiated this wallet.
|
Hub *Hub // A handle to the Hub that instantiated this wallet.
|
||||||
PublicKey []byte // The wallet's public key (used for communication and identification, not signing!)
|
PublicKey []byte // The wallet's public key (used for communication and identification, not signing!)
|
||||||
|
|
||||||
lock sync.Mutex // Lock that gates access to struct fields and communication with the card
|
lock sync.Mutex // Lock that gates access to struct fields and communication with the card
|
||||||
card *scard.Card // A handle to the smartcard interface for the wallet.
|
card *scard.Card // A handle to the smartcard interface for the wallet.
|
||||||
session *Session // The secure communication session with the card
|
session *Session // The secure communication session with the card
|
||||||
log log.Logger // Contextual logger to tag the base with its id
|
log log.Logger // Contextual logger to tag the base with its id
|
||||||
|
|
||||||
deriveNextPath accounts.DerivationPath // Next derivation path for account auto-discovery
|
deriveNextPath accounts.DerivationPath // Next derivation path for account auto-discovery
|
||||||
deriveNextAddr common.Address // Next derived account address for auto-discovery
|
deriveNextAddr common.Address // Next derived account address for auto-discovery
|
||||||
deriveChain ethereum.ChainStateReader // Blockchain state reader to discover used account with
|
deriveChain ethereum.ChainStateReader // Blockchain state reader to discover used account with
|
||||||
@ -273,7 +274,7 @@ func (w *Wallet) Unpair(pin []byte) error {
|
|||||||
func (w *Wallet) URL() accounts.URL {
|
func (w *Wallet) URL() accounts.URL {
|
||||||
return accounts.URL{
|
return accounts.URL{
|
||||||
Scheme: w.Hub.scheme,
|
Scheme: w.Hub.scheme,
|
||||||
Path: fmt.Sprintf("%x", w.PublicKey[1:3]),
|
Path: fmt.Sprintf("%x", w.PublicKey[1:5]), // Byte #0 isn't unique; 1:5 covers << 64K cards, bump to 1:9 for << 4M
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -327,7 +328,7 @@ func (w *Wallet) Open(passphrase string) error {
|
|||||||
if err := w.session.authenticate(*pairing); err != nil {
|
if err := w.session.authenticate(*pairing); err != nil {
|
||||||
return fmt.Errorf("failed to authenticate card %x: %s", w.PublicKey[:4], err)
|
return fmt.Errorf("failed to authenticate card %x: %s", w.PublicKey[:4], err)
|
||||||
}
|
}
|
||||||
return nil
|
return ErrPINNeeded
|
||||||
}
|
}
|
||||||
// If no passphrase was supplied, request the PUK from the user
|
// If no passphrase was supplied, request the PUK from the user
|
||||||
if passphrase == "" {
|
if passphrase == "" {
|
||||||
@ -389,8 +390,8 @@ func (w *Wallet) Close() error {
|
|||||||
// selfDerive is an account derivation loop that upon request attempts to find
|
// selfDerive is an account derivation loop that upon request attempts to find
|
||||||
// new non-zero accounts.
|
// new non-zero accounts.
|
||||||
func (w *Wallet) selfDerive() {
|
func (w *Wallet) selfDerive() {
|
||||||
w.log.Debug("Smartcard wallet self-derivation started")
|
w.log.Debug("Smart card wallet self-derivation started")
|
||||||
defer w.log.Debug("Smartcard wallet self-derivation stopped")
|
defer w.log.Debug("Smart card wallet self-derivation stopped")
|
||||||
|
|
||||||
// Execute self-derivations until termination or error
|
// Execute self-derivations until termination or error
|
||||||
var (
|
var (
|
||||||
@ -418,21 +419,22 @@ func (w *Wallet) selfDerive() {
|
|||||||
|
|
||||||
// Device lock obtained, derive the next batch of accounts
|
// Device lock obtained, derive the next batch of accounts
|
||||||
var (
|
var (
|
||||||
paths []accounts.DerivationPath
|
paths []accounts.DerivationPath
|
||||||
nextAccount accounts.Account
|
nextAcc accounts.Account
|
||||||
nextAddr = w.deriveNextAddr
|
|
||||||
nextPath = w.deriveNextPath
|
nextAddr = w.deriveNextAddr
|
||||||
|
nextPath = w.deriveNextPath
|
||||||
|
|
||||||
context = context.Background()
|
context = context.Background()
|
||||||
)
|
)
|
||||||
for empty := false; !empty; {
|
for empty := false; !empty; {
|
||||||
// Retrieve the next derived Ethereum account
|
// Retrieve the next derived Ethereum account
|
||||||
if nextAddr == (common.Address{}) {
|
if nextAddr == (common.Address{}) {
|
||||||
if nextAccount, err = w.session.derive(nextPath); err != nil {
|
if nextAcc, err = w.session.derive(nextPath); err != nil {
|
||||||
w.log.Warn("Smartcard wallet account derivation failed", "err", err)
|
w.log.Warn("Smartcard wallet account derivation failed", "err", err)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
nextAddr = nextAccount.Address
|
nextAddr = nextAcc.Address
|
||||||
}
|
}
|
||||||
// Check the account's status against the current chain state
|
// Check the account's status against the current chain state
|
||||||
var (
|
var (
|
||||||
@ -459,8 +461,8 @@ func (w *Wallet) selfDerive() {
|
|||||||
paths = append(paths, path)
|
paths = append(paths, path)
|
||||||
|
|
||||||
// Display a log message to the user for new (or previously empty accounts)
|
// Display a log message to the user for new (or previously empty accounts)
|
||||||
if _, known := pairing.Accounts[nextAddr]; !known || (!empty && nextAddr == w.deriveNextAddr) {
|
if _, known := pairing.Accounts[nextAddr]; !known || !empty || nextAddr != w.deriveNextAddr {
|
||||||
w.log.Info("Smartcard wallet discovered new account", "address", nextAccount.Address, "path", path, "balance", balance, "nonce", nonce)
|
w.log.Info("Smartcard wallet discovered new account", "address", nextAddr, "path", path, "balance", balance, "nonce", nonce)
|
||||||
}
|
}
|
||||||
pairing.Accounts[nextAddr] = path
|
pairing.Accounts[nextAddr] = path
|
||||||
|
|
||||||
@ -470,12 +472,10 @@ func (w *Wallet) selfDerive() {
|
|||||||
nextPath[len(nextPath)-1]++
|
nextPath[len(nextPath)-1]++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there are new accounts, write them out
|
// If there are new accounts, write them out
|
||||||
if len(paths) > 0 {
|
if len(paths) > 0 {
|
||||||
err = w.Hub.setPairing(w, pairing)
|
err = w.Hub.setPairing(w, pairing)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shift the self-derivation forward
|
// Shift the self-derivation forward
|
||||||
w.deriveNextAddr = nextAddr
|
w.deriveNextAddr = nextAddr
|
||||||
w.deriveNextPath = nextPath
|
w.deriveNextPath = nextPath
|
||||||
@ -524,6 +524,13 @@ func (w *Wallet) Accounts() []accounts.Account {
|
|||||||
for address, path := range pairing.Accounts {
|
for address, path := range pairing.Accounts {
|
||||||
ret = append(ret, w.makeAccount(address, path))
|
ret = append(ret, w.makeAccount(address, path))
|
||||||
}
|
}
|
||||||
|
for i := 0; i < len(ret); i++ {
|
||||||
|
for j := i + 1; j < len(ret); j++ {
|
||||||
|
if ret[i].URL.Cmp(ret[j].URL) > 0 {
|
||||||
|
ret[i], ret[j] = ret[j], ret[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
Loading…
Reference in New Issue
Block a user