浏览代码

accounts, console: frendly card errors, support pin unblock

Péter Szilágyi 7 年之前
父节点
当前提交
7d5886dcf4
共有 6 个文件被更改,包括 169 次插入112 次删除
  1. 6 20
      accounts/scwallet/apdu.go
  2. 4 9
      accounts/scwallet/hub.go
  3. 14 10
      accounts/scwallet/securechannel.go
  4. 96 73
      accounts/scwallet/wallet.go
  5. 31 0
      accounts/sort.go
  6. 18 0
      console/bridge.go

+ 6 - 20
accounts/scwallet/apdu.go

@@ -21,29 +21,15 @@ import (
 	"encoding/binary"
 )
 
-const (
-	claISO7816 = 0
-
-	insSelect               = 0xA4
-	insGetResponse          = 0xC0
-	insPair                 = 0x12
-	insUnpair               = 0x13
-	insOpenSecureChannel    = 0x10
-	insMutuallyAuthenticate = 0x11
-
-	sw1GetResponse = 0x61
-	sw1Ok          = 0x90
-)
-
-// CommandAPDU represents an application data unit sent to a smartcard.
-type CommandAPDU struct {
+// commandAPDU represents an application data unit sent to a smartcard.
+type commandAPDU struct {
 	Cla, Ins, P1, P2 uint8  // Class, Instruction, Parameter 1, Parameter 2
 	Data             []byte // Command data
 	Le               uint8  // Command data length
 }
 
 // serialize serializes a command APDU.
-func (ca CommandAPDU) serialize() ([]byte, error) {
+func (ca commandAPDU) serialize() ([]byte, error) {
 	buf := new(bytes.Buffer)
 
 	if err := binary.Write(buf, binary.BigEndian, ca.Cla); err != nil {
@@ -72,14 +58,14 @@ func (ca CommandAPDU) serialize() ([]byte, error) {
 	return buf.Bytes(), nil
 }
 
-// ResponseAPDU represents an application data unit received from a smart card.
-type ResponseAPDU struct {
+// responseAPDU represents an application data unit received from a smart card.
+type responseAPDU struct {
 	Data     []byte // response data
 	Sw1, Sw2 uint8  // status words 1 and 2
 }
 
 // deserialize deserializes a response APDU.
-func (ra *ResponseAPDU) deserialize(data []byte) error {
+func (ra *responseAPDU) deserialize(data []byte) error {
 	ra.Data = make([]byte, len(data)-2)
 
 	buf := bytes.NewReader(data)

+ 4 - 9
accounts/scwallet/hub.go

@@ -36,6 +36,7 @@ import (
 	"encoding/json"
 	"io/ioutil"
 	"os"
+	"sort"
 	"sync"
 	"time"
 
@@ -51,7 +52,7 @@ const Scheme = "pcsc"
 
 // refreshCycle is the maximum time between wallet refreshes (if USB hotplug
 // notifications don't work).
-const refreshCycle = 5 * time.Second
+const refreshCycle = time.Second
 
 // refreshThrottling is the minimum time between wallet refreshes to avoid thrashing.
 const refreshThrottling = 500 * time.Millisecond
@@ -132,7 +133,7 @@ func (hub *Hub) writePairings() error {
 	return pairingFile.Close()
 }
 
-func (hub *Hub) getPairing(wallet *Wallet) *smartcardPairing {
+func (hub *Hub) pairing(wallet *Wallet) *smartcardPairing {
 	pairing, ok := hub.pairings[string(wallet.PublicKey)]
 	if ok {
 		return &pairing
@@ -182,13 +183,7 @@ func (hub *Hub) Wallets() []accounts.Wallet {
 	for _, wallet := range hub.wallets {
 		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]
-			}
-		}
-	}
+	sort.Sort(accounts.WalletsByURL(cpy))
 	return cpy
 }
 

+ 14 - 10
accounts/scwallet/securechannel.go

@@ -17,7 +17,6 @@
 package scwallet
 
 import (
-	//"crypto/ecdsa"
 	"bytes"
 	"crypto/aes"
 	"crypto/cipher"
@@ -25,10 +24,10 @@ import (
 	"crypto/sha256"
 	"crypto/sha512"
 	"fmt"
-	//"math/big"
+
 	"github.com/ebfe/scard"
 	"github.com/ethereum/go-ethereum/crypto"
-	ecdh "github.com/wsddn/go-ecdh"
+	"github.com/wsddn/go-ecdh"
 )
 
 const (
@@ -38,6 +37,11 @@ const (
 
 	scSecretLength = 32
 	scBlockSize    = 16
+
+	insOpenSecureChannel    = 0x10
+	insMutuallyAuthenticate = 0x11
+	insPair                 = 0x12
+	insUnpair               = 0x13
 )
 
 // SecureChannelSession enables secure communication with a hardware wallet.
@@ -192,8 +196,8 @@ func (s *SecureChannelSession) mutuallyAuthenticate() error {
 }
 
 // open is an internal method that sends an open APDU.
-func (s *SecureChannelSession) open() (*ResponseAPDU, error) {
-	return transmit(s.card, &CommandAPDU{
+func (s *SecureChannelSession) open() (*responseAPDU, error) {
+	return transmit(s.card, &commandAPDU{
 		Cla:  claSCWallet,
 		Ins:  insOpenSecureChannel,
 		P1:   s.PairingIndex,
@@ -204,8 +208,8 @@ func (s *SecureChannelSession) open() (*ResponseAPDU, error) {
 }
 
 // pair is an internal method that sends a pair APDU.
-func (s *SecureChannelSession) pair(p1 uint8, data []byte) (*ResponseAPDU, error) {
-	return transmit(s.card, &CommandAPDU{
+func (s *SecureChannelSession) pair(p1 uint8, data []byte) (*responseAPDU, error) {
+	return transmit(s.card, &commandAPDU{
 		Cla:  claSCWallet,
 		Ins:  insPair,
 		P1:   p1,
@@ -216,7 +220,7 @@ func (s *SecureChannelSession) pair(p1 uint8, data []byte) (*ResponseAPDU, error
 }
 
 // TransmitEncrypted sends an encrypted message, and decrypts and returns the response.
-func (s *SecureChannelSession) TransmitEncrypted(cla, ins, p1, p2 byte, data []byte) (*ResponseAPDU, error) {
+func (s *SecureChannelSession) TransmitEncrypted(cla, ins, p1, p2 byte, data []byte) (*responseAPDU, error) {
 	if s.iv == nil {
 		return nil, fmt.Errorf("Channel not open")
 	}
@@ -234,7 +238,7 @@ func (s *SecureChannelSession) TransmitEncrypted(cla, ins, p1, p2 byte, data []b
 	copy(fulldata, s.iv)
 	copy(fulldata[len(s.iv):], data)
 
-	response, err := transmit(s.card, &CommandAPDU{
+	response, err := transmit(s.card, &commandAPDU{
 		Cla:  cla,
 		Ins:  ins,
 		P1:   p1,
@@ -260,7 +264,7 @@ func (s *SecureChannelSession) TransmitEncrypted(cla, ins, p1, p2 byte, data []b
 		return nil, fmt.Errorf("Invalid MAC in response")
 	}
 
-	rapdu := &ResponseAPDU{}
+	rapdu := &responseAPDU{}
 	rapdu.deserialize(plainData)
 
 	if rapdu.Sw1 != sw1Ok {

+ 96 - 73
accounts/scwallet/wallet.go

@@ -27,6 +27,7 @@ import (
 	"errors"
 	"fmt"
 	"math/big"
+	"sort"
 	"strings"
 	"sync"
 	"time"
@@ -51,6 +52,12 @@ var ErrPUKNeeded = errors.New("smartcard: puk needed")
 // and send it back.
 var ErrPINNeeded = errors.New("smartcard: pin needed")
 
+// ErrPINUnblockNeeded is returned if opening the smart card requires a PIN code,
+// but all PIN attempts have already been exhausted. In this case the calling
+// application should request user input for the PUK and a new PIN code to set
+// fo the card.
+var ErrPINUnblockNeeded = errors.New("smartcard: pin unblock needed")
+
 // ErrAlreadyOpen is returned if the smart card is attempted to be opened, but
 // there is already a paired and unlocked session.
 var ErrAlreadyOpen = errors.New("smartcard: already open")
@@ -65,8 +72,16 @@ var (
 )
 
 const (
-	claSCWallet           = 0x80
+	claISO7816  = 0
+	claSCWallet = 0x80
+
+	insSelect      = 0xA4
+	insGetResponse = 0xC0
+	sw1GetResponse = 0x61
+	sw1Ok          = 0x90
+
 	insVerifyPin          = 0x20
+	insUnblockPin         = 0x22
 	insExportKey          = 0xC2
 	insSign               = 0xC0
 	insLoadKey            = 0xD0
@@ -117,7 +132,7 @@ func NewWallet(hub *Hub, card *scard.Card) *Wallet {
 // transmit sends an APDU to the smartcard and receives and decodes the response.
 // It automatically handles requests by the card to fetch the return data separately,
 // and returns an error if the response status code is not success.
-func transmit(card *scard.Card, command *CommandAPDU) (*ResponseAPDU, error) {
+func transmit(card *scard.Card, command *commandAPDU) (*responseAPDU, error) {
 	data, err := command.serialize()
 	if err != nil {
 		return nil, err
@@ -128,14 +143,14 @@ func transmit(card *scard.Card, command *CommandAPDU) (*ResponseAPDU, error) {
 		return nil, err
 	}
 
-	response := new(ResponseAPDU)
+	response := new(responseAPDU)
 	if err = response.deserialize(responseData); err != nil {
 		return nil, err
 	}
 
 	// Are we being asked to fetch the response separately?
 	if response.Sw1 == sw1GetResponse && (command.Cla != claISO7816 || command.Ins != insGetResponse) {
-		return transmit(card, &CommandAPDU{
+		return transmit(card, &commandAPDU{
 			Cla:  claISO7816,
 			Ins:  insGetResponse,
 			P1:   0,
@@ -186,7 +201,7 @@ func (w *Wallet) connect() error {
 
 // doselect is an internal (unlocked) function to send a SELECT APDU to the card.
 func (w *Wallet) doselect() (*applicationInfo, error) {
-	response, err := transmit(w.card, &CommandAPDU{
+	response, err := transmit(w.card, &commandAPDU{
 		Cla:  claISO7816,
 		Ins:  insSelect,
 		P1:   4,
@@ -209,13 +224,11 @@ func (w *Wallet) ping() error {
 	w.lock.Lock()
 	defer w.lock.Unlock()
 
+	// We can't ping if not paired
 	if !w.session.paired() {
-		// We can't ping if not paired
 		return nil
 	}
-
-	_, err := w.session.getWalletStatus()
-	if err != nil {
+	if _, err := w.session.walletStatus(); err != nil {
 		return err
 	}
 	return nil
@@ -235,16 +248,13 @@ func (w *Wallet) pair(puk []byte) error {
 	if w.session.paired() {
 		return fmt.Errorf("Wallet already paired")
 	}
-
 	pairing, err := w.session.pair(puk)
 	if err != nil {
 		return err
 	}
-
 	if err = w.Hub.setPairing(w, &pairing); err != nil {
 		return err
 	}
-
 	return w.session.authenticate(pairing)
 }
 
@@ -285,19 +295,26 @@ func (w *Wallet) Status() (string, error) {
 	w.lock.Lock()
 	defer w.lock.Unlock()
 
+	// If the card is not paired, we can only wait
 	if !w.session.paired() {
-		return "Unpaired", nil
+		return "Unpaired, waiting for PUK", nil
 	}
-
-	status, err := w.session.getWalletStatus()
+	// Yay, we have an encrypted session, retrieve the actual status
+	status, err := w.session.walletStatus()
 	if err != nil {
-		return "Error", err
-	}
-
-	if w.session.verified {
-		return fmt.Sprintf("Open, %s", status), nil
-	} else {
-		return fmt.Sprintf("Locked, %s", status), nil
+		return fmt.Sprintf("Failed: %v", err), err
+	}
+	switch {
+	case !w.session.verified && status.PinRetryCount == 0:
+		return fmt.Sprintf("Blocked, waiting for PUK and new PIN"), nil
+	case !w.session.verified:
+		return fmt.Sprintf("Locked, waiting for PIN (%d attempts left)", status.PinRetryCount), nil
+	case !status.Initialized:
+		return fmt.Sprintf("Empty, waiting for initialization"), nil
+	case status.SupportsPKDerivation:
+		return fmt.Sprintf("Online, can derive public keys"), nil
+	default:
+		return fmt.Sprintf("Online, cannot derive public keys"), nil
 	}
 }
 
@@ -324,29 +341,46 @@ func (w *Wallet) Open(passphrase string) error {
 	// pairing key or form the supplied PUK code.
 	if !w.session.paired() {
 		// If a previous pairing exists, only ever try to use that
-		if pairing := w.Hub.getPairing(w); pairing != nil {
+		if pairing := w.Hub.pairing(w); pairing != nil {
 			if err := w.session.authenticate(*pairing); err != nil {
 				return fmt.Errorf("failed to authenticate card %x: %s", w.PublicKey[:4], err)
 			}
-			return ErrPINNeeded
+			// Pairing still ok, fall through to PIN checks
+		} else {
+			// If no passphrase was supplied, request the PUK from the user
+			if passphrase == "" {
+				return ErrPUKNeeded
+			}
+			// Attempt to pair the smart card with the user supplied PUK
+			if err := w.pair([]byte(passphrase)); err != nil {
+				return err
+			}
+			// Pairing succeeded, fall through to PIN checks. This will of course fail,
+			// but we can't return ErrPINNeeded directly here becase we don't know whether
+			// a PIN check or a PIN reset is needed.
+			passphrase = ""
 		}
-		// If no passphrase was supplied, request the PUK from the user
-		if passphrase == "" {
-			return ErrPUKNeeded
+	}
+	// The smart card was successfully paired, retrieve its status to check whether
+	// PIN verification or unblocking is needed.
+	status, err := w.session.walletStatus()
+	if err != nil {
+		return err
+	}
+	// Request the appropriate next authentication data, or use the one supplied
+	switch {
+	case passphrase == "" && status.PinRetryCount > 0:
+		return ErrPINNeeded
+	case passphrase == "":
+		return ErrPINUnblockNeeded
+	case status.PinRetryCount > 0:
+		if err := w.session.verifyPin([]byte(passphrase)); err != nil {
+			return err
 		}
-		// Attempt to pair the smart card with the user supplied PUK
-		if err := w.pair([]byte(passphrase)); err != nil {
+	default:
+		if err := w.session.unblockPin([]byte(passphrase)); err != nil {
 			return err
 		}
-		return ErrPINNeeded // We always need the PIN after the PUK
-	}
-	// The smart card was successfully paired, request a PIN code or use the one
-	// supplied by the user
-	if passphrase == "" {
-		return ErrPINNeeded
-	}
-	if err := w.session.verifyPin([]byte(passphrase)); err != nil {
-		return err
 	}
 	// Smart card paired and unlocked, initialize and register
 	w.deriveReq = make(chan chan struct{})
@@ -415,7 +449,7 @@ func (w *Wallet) selfDerive() {
 			reqc <- struct{}{}
 			continue
 		}
-		pairing := w.Hub.getPairing(w)
+		pairing := w.Hub.pairing(w)
 
 		// Device lock obtained, derive the next batch of accounts
 		var (
@@ -519,18 +553,12 @@ func (w *Wallet) Accounts() []accounts.Account {
 	w.lock.Lock()
 	defer w.lock.Unlock()
 
-	if pairing := w.Hub.getPairing(w); pairing != nil {
+	if pairing := w.Hub.pairing(w); pairing != nil {
 		ret := make([]accounts.Account, 0, len(pairing.Accounts))
 		for address, path := range pairing.Accounts {
 			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]
-				}
-			}
-		}
+		sort.Sort(accounts.AccountsByURL(ret))
 		return ret
 	}
 	return nil
@@ -548,7 +576,7 @@ func (w *Wallet) makeAccount(address common.Address, path accounts.DerivationPat
 
 // Contains returns whether an account is part of this particular wallet or not.
 func (w *Wallet) Contains(account accounts.Account) bool {
-	if pairing := w.Hub.getPairing(w); pairing != nil {
+	if pairing := w.Hub.pairing(w); pairing != nil {
 		_, ok := pairing.Accounts[account.Address]
 		return ok
 	}
@@ -576,7 +604,7 @@ func (w *Wallet) Derive(path accounts.DerivationPath, pin bool) (accounts.Accoun
 	}
 
 	if pin {
-		pairing := w.Hub.getPairing(w)
+		pairing := w.Hub.pairing(w)
 		pairing.Accounts[account.Address] = path
 		if err := w.Hub.setPairing(w, pairing); err != nil {
 			return accounts.Account{}, err
@@ -684,7 +712,7 @@ func (w *Wallet) SignTxWithPassphrase(account accounts.Account, passphrase strin
 // It first checks for the address in the list of pinned accounts, and if it is
 // not found, attempts to parse the derivation path from the account's URL.
 func (w *Wallet) findAccountPath(account accounts.Account) (accounts.DerivationPath, error) {
-	pairing := w.Hub.getPairing(w)
+	pairing := w.Hub.pairing(w)
 	if path, ok := pairing.Accounts[account.Address]; ok {
 		return path, nil
 	}
@@ -745,6 +773,16 @@ func (s *Session) verifyPin(pin []byte) error {
 	return nil
 }
 
+// unblockPin unblocks a wallet with the provided puk and resets the pin to the
+// new one specified.
+func (s *Session) unblockPin(pukpin []byte) error {
+	if _, err := s.Channel.TransmitEncrypted(claSCWallet, insUnblockPin, 0, 0, pukpin); err != nil {
+		return err
+	}
+	s.verified = true
+	return nil
+}
+
 // release releases resources associated with the channel.
 func (s *Session) release() error {
 	return s.Wallet.card.Disconnect(scard.LeaveCard)
@@ -773,32 +811,25 @@ type walletStatus struct {
 	SupportsPKDerivation bool // Whether the card supports doing public key derivation itself
 }
 
-func (w walletStatus) String() string {
-	return fmt.Sprintf("pinRetryCount=%d, pukRetryCount=%d, initialized=%t, supportsPkDerivation=%t", w.PinRetryCount, w.PukRetryCount, w.Initialized, w.SupportsPKDerivation)
-}
-
-// getWalletStatus fetches the wallet's status from the card.
-func (s *Session) getWalletStatus() (*walletStatus, error) {
+// walletStatus fetches the wallet's status from the card.
+func (s *Session) walletStatus() (*walletStatus, error) {
 	response, err := s.Channel.TransmitEncrypted(claSCWallet, insStatus, statusP1WalletStatus, 0, nil)
 	if err != nil {
 		return nil, err
 	}
-
 	status := new(walletStatus)
 	if _, err := asn1.UnmarshalWithParams(response.Data, status, "tag:3"); err != nil {
 		return nil, err
 	}
-
 	return status, nil
 }
 
-// getDerivationPath fetches the wallet's current derivation path from the card.
-func (s *Session) getDerivationPath() (accounts.DerivationPath, error) {
+// derivationPath fetches the wallet's current derivation path from the card.
+func (s *Session) derivationPath() (accounts.DerivationPath, error) {
 	response, err := s.Channel.TransmitEncrypted(claSCWallet, insStatus, statusP1Path, 0, nil)
 	if err != nil {
 		return nil, err
 	}
-
 	buf := bytes.NewReader(response.Data)
 	path := make(accounts.DerivationPath, len(response.Data)/4)
 	return path, binary.Read(buf, binary.BigEndian, &path)
@@ -845,11 +876,11 @@ func (s *Session) derive(path accounts.DerivationPath) (accounts.Account, error)
 	// start again.
 	remainingPath := path
 
-	pubkey, err := s.getPublicKey()
+	pubkey, err := s.publicKey()
 	if err != nil {
 		return accounts.Account{}, err
 	}
-	currentPath, err := s.getDerivationPath()
+	currentPath, err := s.derivationPath()
 	if err != nil {
 		return accounts.Account{}, err
 	}
@@ -910,7 +941,6 @@ func (s *Session) deriveKeyAssisted(reset bool, pathComponent uint32) ([]byte, e
 	if _, err := asn1.UnmarshalWithParams(response.Data, keyinfo, "tag:2"); err != nil {
 		return nil, err
 	}
-
 	rbytes, sbytes := keyinfo.Signature.R.Bytes(), keyinfo.Signature.S.Bytes()
 	sig := make([]byte, 65)
 	copy(sig[32-len(rbytes):32], rbytes)
@@ -920,12 +950,10 @@ func (s *Session) deriveKeyAssisted(reset bool, pathComponent uint32) ([]byte, e
 	if err != nil {
 		return nil, err
 	}
-
 	_, err = s.Channel.TransmitEncrypted(claSCWallet, insDeriveKey, deriveP1Assisted|deriveP1Append, deriveP2PublicKey, pubkey)
 	if err != nil {
 		return nil, err
 	}
-
 	return pubkey, nil
 }
 
@@ -935,18 +963,16 @@ type keyExport struct {
 	PrivateKey []byte `asn1:"tag:1,optional"`
 }
 
-// getPublicKey returns the public key for the current derivation path.
-func (s *Session) getPublicKey() ([]byte, error) {
+// publicKey returns the public key for the current derivation path.
+func (s *Session) publicKey() ([]byte, error) {
 	response, err := s.Channel.TransmitEncrypted(claSCWallet, insExportKey, exportP1Any, exportP2Pubkey, nil)
 	if err != nil {
 		return nil, err
 	}
-
 	keys := new(keyExport)
 	if _, err := asn1.UnmarshalWithParams(response.Data, keys, "tag:1"); err != nil {
 		return nil, err
 	}
-
 	return keys.PublicKey, nil
 }
 
@@ -974,12 +1000,10 @@ func (s *Session) sign(path accounts.DerivationPath, hash []byte) ([]byte, error
 	if err != nil {
 		return nil, err
 	}
-
 	sigdata := new(signatureData)
 	if _, err := asn1.UnmarshalWithParams(response.Data, sigdata, "tag:0"); err != nil {
 		return nil, err
 	}
-
 	// Serialize the signature
 	rbytes, sbytes := sigdata.Signature.R.Bytes(), sigdata.Signature.S.Bytes()
 	sig := make([]byte, 65)
@@ -991,7 +1015,6 @@ func (s *Session) sign(path accounts.DerivationPath, hash []byte) ([]byte, error
 	if err != nil {
 		return nil, err
 	}
-
 	log.Debug("Signed using smartcard", "deriveTime", deriveTime.Sub(startTime), "signingTime", time.Since(deriveTime))
 
 	return sig, nil

+ 31 - 0
accounts/sort.go

@@ -0,0 +1,31 @@
+// Copyright 2018 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 accounts
+
+// AccountsByURL implements sort.Interface for []Account based on the URL field.
+type AccountsByURL []Account
+
+func (a AccountsByURL) Len() int           { return len(a) }
+func (a AccountsByURL) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
+func (a AccountsByURL) Less(i, j int) bool { return a[i].URL.Cmp(a[j].URL) < 0 }
+
+// WalletsByURL implements sort.Interface for []Wallet based on the URL field.
+type WalletsByURL []Wallet
+
+func (w WalletsByURL) Len() int           { return len(w) }
+func (w WalletsByURL) Swap(i, j int)      { w[i], w[j] = w[j], w[i] }
+func (w WalletsByURL) Less(i, j int) bool { return w[i].URL().Cmp(w[j].URL()) < 0 }

+ 18 - 0
console/bridge.go

@@ -141,6 +141,24 @@ func (b *bridge) OpenWallet(call otto.FunctionCall) (response otto.Value) {
 			}
 		}
 
+	case strings.HasSuffix(err.Error(), scwallet.ErrPINUnblockNeeded.Error()):
+		// PIN unblock requested, fetch PUK and new PIN from the user
+		var pukpin string
+		if input, err := b.prompter.PromptPassword("Please enter current PUK: "); err != nil {
+			throwJSException(err.Error())
+		} else {
+			pukpin = input
+		}
+		if input, err := b.prompter.PromptPassword("Please enter new PIN: "); err != nil {
+			throwJSException(err.Error())
+		} else {
+			pukpin += input
+		}
+		passwd, _ = otto.ToValue(pukpin)
+		if val, err = call.Otto.Call("jeth.openWallet", nil, wallet, passwd); err != nil {
+			throwJSException(err.Error())
+		}
+
 	case strings.HasSuffix(err.Error(), scwallet.ErrPINNeeded.Error()):
 		// PIN input requested, fetch from the user and call open again
 		if input, err := b.prompter.PromptPassword("Please enter current PIN: "); err != nil {