diff --git a/contrib/build-linux/appimage/Dockerfile b/contrib/build-linux/appimage/Dockerfile
index 52ad7520a46c..53d83092faf9 100644
--- a/contrib/build-linux/appimage/Dockerfile
+++ b/contrib/build-linux/appimage/Dockerfile
@@ -17,6 +17,8 @@ RUN apt-get update -q && \
zlib1g-dev=1:1.2.8.dfsg-2ubuntu4.3 \
libffi-dev=3.2.1-4 \
libncurses5-dev=6.0+20160213-1ubuntu1 \
+ libpcsclite-dev=1.8.14-1ubuntu1.16.04.1 \
+ swig=3.0.8-0ubuntu3 \
libsqlite3-dev=3.11.0-1ubuntu1.5 \
libusb-1.0-0-dev=2:1.0.20-1 \
libudev-dev=229-4ubuntu21.29 \
diff --git a/contrib/build-wine/deterministic.spec b/contrib/build-wine/deterministic.spec
index 457897d12603..be677a354e8c 100644
--- a/contrib/build-wine/deterministic.spec
+++ b/contrib/build-wine/deterministic.spec
@@ -39,7 +39,7 @@ binaries += [('C:/tmp/libusb-1.0.dll', '.')]
binaries += [('C:/tmp/libzbar-0.dll', '.')]
# pyscard binaries for Satochip
-binaries += [('C:/python*/Lib/site-packages/smartcard/scard/_scard.cp36-win32.pyd', '.')] #satochip
+binaries += [('C:/python*/Lib/site-packages/smartcard/scard/_scard.cp37-win32.pyd', '.')] #satochip
datas = [
(home+'electrum_ltc/*.json', 'electrum_ltc'),
diff --git a/contrib/build-wine/prepare-wine.sh b/contrib/build-wine/prepare-wine.sh
index caf9a64c0eb3..bfd908c0e167 100755
--- a/contrib/build-wine/prepare-wine.sh
+++ b/contrib/build-wine/prepare-wine.sh
@@ -13,16 +13,15 @@ PYINSTALLER_REPO="https://github.com/SomberNight/pyinstaller.git"
PYINSTALLER_COMMIT="6e455b2c1208465742484436009bfb1e1baf2e01"
# ^ tag 4.0, plus a custom commit that fixes cross-compilation with MinGW
-PYTHON_VERSION=3.7.9
-
-#Satochip pyscard (from https://ci.appveyor.com/project/LudovicRousseau/pyscard)
+# pyscard prebuilt binaries for Satochip
# PYSCARD_FILENAME=pyscard-1.9.9-cp36-cp36m-win32.whl # python 3.6, 32-bit
-# PYSCARD_URL=https://ci.appveyor.com/api/buildjobs/3uiua5o4llvpegbp/artifacts/dist/pyscard-1.9.9-cp36-cp36m-win32.whl
+# PYSCARD_URL=https://github.com/cculianu/Electron-Cash-Build-Tools/releases/download/v1.0/pyscard-1.9.9-cp36-cp36m-win32.whl
# PYSCARD_SHA256=99d2b450f322f9ed9682fd2a99d95ce781527e371006cded38327efca8158fe7
-PYSCARD_FILENAME=pyscard-1.9.9-cp36-cp36m-win32.whl # python 3.6, 32-bit
-PYSCARD_URL=https://github.com/cculianu/Electron-Cash-Build-Tools/releases/download/v1.0/pyscard-1.9.9-cp36-cp36m-win32.whl
-PYSCARD_SHA256=99d2b450f322f9ed9682fd2a99d95ce781527e371006cded38327efca8158fe7
+PYSCARD_FILENAME=pyscard-1.9.9-cp37-cp37m-win32.whl # python 3.7, 32-bit
+PYSCARD_URL=https://ci.appveyor.com/api/buildjobs/f9cmce4j8hkau9n4/artifacts/dist/pyscard-1.9.9-cp37-cp37m-win32.whl
+PYSCARD_SHA256=3f7d52dd6694dd369b02e797fe1a3e39b63cf1d1c4b5fc0e1341aafa24f87e7a
+PYTHON_VERSION=3.7.9
## These settings probably don't need change
export WINEPREFIX=/opt/wine64
diff --git a/contrib/deterministic-build/requirements-hw.txt b/contrib/deterministic-build/requirements-hw.txt
index 5f9b12b4bfe2..110c94a681de 100644
--- a/contrib/deterministic-build/requirements-hw.txt
+++ b/contrib/deterministic-build/requirements-hw.txt
@@ -141,9 +141,9 @@ pyaes==1.6.1 \
pycparser==2.20 \
--hash=sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0 \
--hash=sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705
-pysatochip==0.11.3 \
- --hash=sha256:f115b659b80304ef0dec650d753c2d0ba6f0512a2ad6be9bdef291e0d6307ec4 \
- --hash=sha256:dcf6ac88d15570941543ea46ba0360b75ec216b1253354b4ba863473e95850fc
+pysatochip==0.11.4 \
+ --hash=sha256:89ac8b7936c51c96a3fe4a7b4c602a4a4b20ea36a935cc43c268ddcbbd90203e \
+ --hash=sha256:263ff36484bd697999db1762dbd12e9fb89122b224f68065aed37dfbaa2920ca
pyscard==1.9.9 \
--hash=sha256:6620a74f58d5fa9076544263bb4e42c946eb20f315c896d14a7e5743d5431469 \
--hash=sha256:a047738c58d05b4dab15aa9c99fbd92f8d0670900de89c68bec247a422f8d8c7 \
diff --git a/contrib/requirements/requirements-hw.txt b/contrib/requirements/requirements-hw.txt
index 9f38290d5a91..663df31c4f8a 100644
--- a/contrib/requirements/requirements-hw.txt
+++ b/contrib/requirements/requirements-hw.txt
@@ -6,4 +6,6 @@ pysatochip >=0.11.3
keepkey>=6.3.1
btchip-python>=0.1.30
ckcc-protocol>=0.7.7
+pyscard>=1.9.9
+pysatochip==0.11.4
bitbox02>=5.0.0
\ No newline at end of file
diff --git a/electrum_ltc/plugins/hw_wallet/qt.py b/electrum_ltc/plugins/hw_wallet/qt.py
index c6919a521edf..1320d66db1e3 100644
--- a/electrum_ltc/plugins/hw_wallet/qt.py
+++ b/electrum_ltc/plugins/hw_wallet/qt.py
@@ -187,9 +187,10 @@ def error_dialog(self, msg, blocking):
def clear_dialog(self):
if self.dialog:
- try: self.dialog.accept()
+ try:
+ self.dialog.accept()
except RuntimeError:
- pass #see https://github.com/Electron-Cash/Electron-Cash/issues/1437
+ pass #see https://github.com/Electron-Cash/Electron-Cash/issues/1437
self.dialog = None
def win_query_choice(self, msg, labels):
diff --git a/electrum_ltc/plugins/satochip/README.rst b/electrum_ltc/plugins/satochip/README.rst
index 32211ee76026..56f7d32adc5b 100644
--- a/electrum_ltc/plugins/satochip/README.rst
+++ b/electrum_ltc/plugins/satochip/README.rst
@@ -14,7 +14,7 @@ Introduction
This plugin allows to integrate the Satochip Hardware Wallet with Electrum. To use it, you need a device with the Satochip javacard applet installed (see https://github.com/Toporin/SatochipApplet).
If the wallet is not intialized yet, Electrum will perform the setup (you only need to do this once). During setup, a seed is created: this seed allows you to recover your wallet at anytime, so make sure to BACKUP THE SEED SECURELY! During setup, a PIN code is also created: this PIN allows to unlock th device to access your funds. If you try too many wrong PIN, your device will be locked indefinitely (it is 'bricked'). If you loose your PIN or brick your device, you can only recover your funds with the seed backup.
-The Satochip wallet is currently in Beta, use with caution! In this phase, it is strongly recommended to use the software on the Bitcoin testnet first.
+The Satochip wallet is currently in Beta, use with caution!You can use the software on the Bitcoin testnet using the --testnet option.
This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software.
Rem: Electrum uses Python 3.x. In case of error, check first that you are not trying to run Electrum with Python 2.x or with Python 2.x libraries.
@@ -81,17 +81,4 @@ Pyscard is required to connect to the smartcard::
To run Electrum use::
- python3 electrum -v --testnet
-
-
-Test suite
-=============
-
-To run the test suite, run::
-
- python -m unittest electrum.plugins.satochip.test_CardConnector
-
-The test suite uses the following default PIN code: "12345678".
-If you run the test suite after (or before) electrum, you may block the card if the PIN used are not the same!
-If the card is locked, you will have to reinstall the javacard applet on the card.
-
+ python3 electrum -v --testnet
\ No newline at end of file
diff --git a/electrum_ltc/plugins/satochip/qt.py b/electrum_ltc/plugins/satochip/qt.py
index c43908def18a..c1e7b2f7a0a0 100644
--- a/electrum_ltc/plugins/satochip/qt.py
+++ b/electrum_ltc/plugins/satochip/qt.py
@@ -1,9 +1,12 @@
from electrum_ltc.i18n import _
from electrum_ltc.logging import get_logger
+from electrum_ltc.simple_config import SimpleConfig
from electrum_ltc.gui.qt.util import (EnterButton, Buttons, CloseButton, OkButton, CancelButton, WindowModalDialog, WWLabel)
-from PyQt5.QtCore import Qt, pyqtSignal
+from electrum_ltc.gui.qt.qrcodewidget import QRCodeWidget, QRDialog
+from PyQt5.QtCore import Qt, pyqtSignal
from PyQt5.QtWidgets import (QPushButton, QLabel, QVBoxLayout, QWidget, QGridLayout, QLineEdit, QCheckBox)
from functools import partial
+from os import urandom
#satochip
from .satochip import SatochipPlugin
@@ -17,6 +20,8 @@
_logger = get_logger(__name__)
+MSG_USE_2FA= _("Do you want to use 2-Factor-Authentication (2FA)?\n\nWith 2FA, any transaction must be confirmed on a second device such as your smartphone. First you have to install the Satochip-2FA android app on google play. Then you have to pair your 2FA device with your Satochip by scanning the qr-code on the next screen. \n\nWARNING: be sure to backup a copy of the qr-code in a safe place, in case you have to reinstall the app!")
+
class Plugin(SatochipPlugin, QtPluginBase):
icon_unpaired = "satochip_unpaired.png"
icon_paired = "satochip.png"
@@ -49,10 +54,17 @@ def settings_dialog(self, window):
def show_settings_dialog(self, window, keystore):
# When they click on the icon for Satochip we come here.
- device_id = self.choose_device(window, keystore)
- if device_id:
- SatochipSettingsDialog(window, self, keystore, device_id).exec_()
-
+ # device_id = self.choose_device(window, keystore)
+ # if device_id:
+ # SatochipSettingsDialog(window, self, keystore, device_id).exec_()
+ def connect():
+ device_id = self.choose_device(window, keystore)
+ return device_id
+ def show_dialog(device_id):
+ if device_id:
+ SatochipSettingsDialog(window, self, keystore, device_id).exec_()
+ keystore.thread.add(connect, on_success=show_dialog)
+
class Satochip_Handler(QtHandlerBase):
def __init__(self, win):
@@ -93,7 +105,7 @@ def connect_and_doit():
satochip.io''')
title.setTextInteractionFlags(Qt.LinksAccessibleByMouse)
- grid.addWidget(title , 0,0, 1,2, Qt.AlignHCenter)
+ grid.addWidget(title, 0, 0, 1, 2, Qt.AlignHCenter)
y = 3
rows = [
@@ -119,18 +131,35 @@ def _change_pin():
thread.add(connect_and_doit, on_success=self.change_pin)
pin_btn.clicked.connect(_change_pin)
- seed_btn = QPushButton('reset seed')
+ seed_btn = QPushButton('Reset seed')
def _reset_seed():
thread.add(connect_and_doit, on_success=self.reset_seed)
thread.add(connect_and_doit, on_success=self.show_values)
seed_btn.clicked.connect(_reset_seed)
-
+
+ set_2FA_btn = QPushButton('Enable 2FA')
+ def _set_2FA():
+ thread.add(connect_and_doit, on_success=self.set_2FA)
+ thread.add(connect_and_doit, on_success=self.show_values)
+ set_2FA_btn.clicked.connect(_set_2FA)
+
+ reset_2FA_btn = QPushButton('Disable 2FA')
+ def _reset_2FA():
+ thread.add(connect_and_doit, on_success=self.reset_2FA)
+ thread.add(connect_and_doit, on_success=self.show_values)
+ reset_2FA_btn.clicked.connect(_reset_2FA)
+
y += 3
- grid.addWidget(pin_btn, y, 0)
- grid.addWidget(seed_btn, y, 1)
- y += 5
- grid.addWidget(CloseButton(self), y, 1)
-
+ grid.addWidget(pin_btn, y, 0, 1, 2, Qt.AlignHCenter)
+ y += 2
+ grid.addWidget(seed_btn, y, 0, 1, 2, Qt.AlignHCenter)
+ y += 2
+ grid.addWidget(set_2FA_btn, y, 0, 1, 2, Qt.AlignHCenter)
+ y += 2
+ grid.addWidget(reset_2FA_btn, y, 0, 1, 2, Qt.AlignHCenter)
+ y += 2
+ grid.addWidget(CloseButton(self), y, 0, 1, 2, Qt.AlignHCenter)
+
dialog_vbox = QVBoxLayout(self)
dialog_vbox.addWidget(body)
@@ -161,7 +190,7 @@ def show_values(self, client):
# needs2FA?
if d["needs2FA"]:
self.needs_2FA.setText('%s' % "yes")
- elif len(response)>=9 and response[8]==0X00:
+ else:
self.needs_2FA.setText('%s' % "no")
# needs secure channel
@@ -176,7 +205,7 @@ def show_values(self, client):
self.needs_2FA.setText('%s' % "(unitialized)")
self.is_seeded.setText('%s' % "no")
self.needs_SC.setText('%s' % "(unknown)")
-
+
def change_pin(self, client):
_logger.info("In change_pin")
@@ -209,7 +238,7 @@ def reset_seed(self, client):
_("Please be sure that your wallet is empty and that you have a backup of the seed as a precaution.\n\n"),
_("To proceed, enter the PIN for your Satochip:")
])
- (password, reset_2FA)= self.reset_seed_dialog(msg)
+ password = self.reset_seed_dialog(msg)
if (password is None):
return
pin = password.encode('utf8')
@@ -253,11 +282,58 @@ def reset_seed(self, client):
msg= _("Seed reset successfully!\nYou should close this wallet and launch the wizard to generate a new wallet.")
client.handler.show_message(msg)
#to do: close client?
+ elif (sw1==0x9c and sw2==0x0b):
+ msg= _(f"Failed to reset seed: request rejected by 2FA device (error code: {hex(256*sw1+sw2)})")
+ client.handler.show_message(msg)
+ #to do: close client?
else:
- msg= _(f"Failed to reset seed with error code: {hex(sw1)}{hex(sw2)}")
- client.handler.show_error(msg)
-
- if reset_2FA and client.cc.needs_2FA:
+ msg= _(f"Failed to reset seed with error code: {hex(256*sw1+sw2)}")
+ client.handler.show_error(msg)
+
+ def reset_seed_dialog(self, msg):
+ _logger.info("In reset_seed_dialog")
+ parent = self.top_level_window()
+ d = WindowModalDialog(parent, _("Enter PIN"))
+ pw = QLineEdit()
+ pw.setEchoMode(2)
+ pw.setMinimumWidth(200)
+
+ vbox = QVBoxLayout()
+ vbox.addWidget(WWLabel(msg))
+ vbox.addWidget(pw)
+ vbox.addLayout(Buttons(CancelButton(d), OkButton(d)))
+ d.setLayout(vbox)
+
+ passphrase = pw.text() if d.exec_() else None
+ return passphrase
+
+ def set_2FA(self, client):
+ if not client.cc.needs_2FA:
+ use_2FA=client.handler.yes_no_question(MSG_USE_2FA)
+ if (use_2FA):
+ secret_2FA= urandom(20)
+ secret_2FA_hex=secret_2FA.hex()
+ # the secret must be shared with the second factor app (eg on a smartphone)
+ try:
+ config = SimpleConfig()
+ help_txt="Scan the QR-code with your Satochip-2FA app and make a backup of the following secret: "+ secret_2FA_hex
+ d = QRDialog(data=secret_2FA_hex, parent=None, title="Secret_2FA", show_text=False, help_text=help_txt, show_copy_text_btn=True, config=config)
+ d.exec_()
+ except Exception as e:
+ _logger.info("SatochipPlugin: setup 2FA error: "+str(e))
+ return
+ # further communications will require an id and an encryption key (for privacy).
+ # Both are derived from the secret_2FA using a one-way function inside the Satochip
+ amount_limit= 0 # i.e. always use
+ (response, sw1, sw2)=client.cc.card_set_2FA_key(secret_2FA, amount_limit)
+ if sw1!=0x90 or sw2!=0x00:
+ _logger.info(f"Unable to set 2FA with error code:= {hex(256*sw1+sw2)}")#debugSatochip
+ raise RuntimeError(f'Unable to setup 2FA with error code: {hex(256*sw1+sw2)}')
+ else:
+ client.handler.show_message("2FA enabled successfully!")
+
+ def reset_2FA(self, client):
+ if client.cc.needs_2FA:
# challenge based on ID_2FA
# format & encrypt msg
import json
@@ -276,7 +352,7 @@ def reset_seed(self, client):
try:
reply_encrypt= d['reply_encrypt']
except Exception as e:
- self.give_error("No response received from 2FA.\nPlease ensure that the Satochip-2FA plugin is enabled in Tools>Optional Features", True)
+ self.give_error("No response received from 2FA!", True)
reply_decrypt= client.cc.card_crypt_transaction_2FA(reply_encrypt, False)
_logger.info("challenge:response= "+ reply_decrypt)
reply_decrypt= reply_decrypt.split(":")
@@ -289,28 +365,31 @@ def reset_seed(self, client):
msg= _("2FA reset successfully!")
client.cc.needs_2FA= False
client.handler.show_message(msg)
+ elif (sw1==0x9c and sw2==0x17):
+ msg= _(f"Failed to reset 2FA: \nyou must reset the seed first (error code {hex(256*sw1+sw2)})")
+ client.handler.show_error(msg)
else:
- msg= _(f"Failed to reset 2FA with error code: {hex(sw1)}{hex(sw2)}")
+ msg= _(f"Failed to reset 2FA with error code: {hex(256*sw1+sw2)}")
client.handler.show_error(msg)
+ else:
+ msg= _(f"2FA is already disabled!")
+ client.handler.show_error(msg)
+
+
+
+
+
- def reset_seed_dialog(self, msg):
- _logger.info("In reset_seed_dialog")
- parent = self.top_level_window()
- d = WindowModalDialog(parent, _("Enter PIN"))
- pw = QLineEdit()
- pw.setEchoMode(2)
- pw.setMinimumWidth(200)
-
- cb_reset_2FA = QCheckBox(_('Also reset 2FA'))
+
- vbox = QVBoxLayout()
- vbox.addWidget(WWLabel(msg))
- vbox.addWidget(pw)
- vbox.addWidget(cb_reset_2FA)
- vbox.addLayout(Buttons(CancelButton(d), OkButton(d)))
- d.setLayout(vbox)
+
+
+
+
+
+
- passphrase = pw.text() if d.exec_() else None
- reset_2FA= cb_reset_2FA.isChecked()
- return (passphrase, reset_2FA)
+
+
+
diff --git a/electrum_ltc/plugins/satochip/satochip.py b/electrum_ltc/plugins/satochip/satochip.py
index 1d54d343472f..9e7af3c828bb 100644
--- a/electrum_ltc/plugins/satochip/satochip.py
+++ b/electrum_ltc/plugins/satochip/satochip.py
@@ -1,719 +1,772 @@
-from os import urandom
-import hashlib
-
-#electrum
-from electrum_ltc import mnemonic
-from electrum_ltc import constants
-from electrum_ltc.bitcoin import TYPE_ADDRESS, int_to_hex, var_int
-from electrum_ltc.i18n import _
-from electrum_ltc.plugin import BasePlugin, Device, run_hook
-from electrum_ltc.keystore import Hardware_KeyStore, bip39_to_seed
-from electrum_ltc.transaction import Transaction
-from electrum_ltc.wallet import Standard_Wallet
-from electrum_ltc.util import bfh, bh2u, versiontuple
-from electrum_ltc.base_wizard import ScriptTypeNotSupported
-from electrum_ltc.crypto import hash_160, sha256d
-from electrum_ltc.ecc import CURVE_ORDER, der_sig_from_r_and_s, get_r_and_s_from_der_sig, ECPubkey
-from electrum_ltc.mnemonic import Mnemonic
-from electrum_ltc.bip32 import BIP32Node, convert_bip32_path_to_list_of_uint32
-from electrum_ltc.logging import get_logger
-from electrum_ltc.gui.qt.qrcodewidget import QRCodeWidget, QRDialog
-
-from ..hw_wallet import HW_PluginBase
-
-#pysatochip
-from pysatochip.CardConnector import CardConnector, UninitializedSeedError
-from pysatochip.JCconstants import JCconstants
-from pysatochip.TxParser import TxParser
-from pysatochip.Satochip2FA import Satochip2FA
-from pysatochip.version import SATOCHIP_PROTOCOL_MAJOR_VERSION, SATOCHIP_PROTOCOL_MINOR_VERSION, SATOCHIP_PROTOCOL_VERSION
-
-#pyscard
-from smartcard.sw.SWExceptions import SWException
-from smartcard.Exceptions import CardConnectionException, CardRequestTimeoutException
-from smartcard.CardType import AnyCardType
-from smartcard.CardRequest import CardRequest
-
-_logger = get_logger(__name__)
-
-# debug: smartcard reader ids
-SATOCHIP_VID= 0 #0x096E
-SATOCHIP_PID= 0 #0x0503
-
-MSG_USE_2FA= _("Do you want to use 2-Factor-Authentication (2FA)?\n\nWith 2FA, any transaction must be confirmed on a second device such as your smartphone. First you have to install the Satochip-2FA android app on google play. Then you have to pair your 2FA device with your Satochip by scanning the qr-code on the next screen. Warning: be sure to backup a copy of the qr-code in a safe place, in case you have to reinstall the app!")
-
-# def bip32path2bytes(bip32path:str) -> (int, bytes):
- # splitPath = bip32path.split('/')
- # splitPath=[x for x in splitPath if x] # removes empty values
- # if splitPath[0] == 'm':
- # splitPath = splitPath[1:]
-
- # bytePath=b''
- # depth= len(splitPath)
- # for index in splitPath:
- # if index.endswith("'"):
- # bytePath+= pack( ">I", int(index.rstrip("'"))+0x80000000 )
- # else:
- # bytePath+=pack( ">I", int(index) )
-
- # return (depth, bytePath)
-
-def bip32path2bytes(bip32path:str) -> (int, bytes):
- intPath= convert_bip32_path_to_list_of_uint32(bip32path)
- depth= len(intPath)
- bytePath=b''
- for index in intPath:
- bytePath+= index.to_bytes(4, byteorder='big', signed=False)
- return (depth, bytePath)
-
-class SatochipClient():
- def __init__(self, plugin, handler):
- _logger.info(f"[SatochipClient] __init__()")#debugSatochip
- self.device = plugin.device
- self.handler = handler
- #self.parser= CardDataParser()
- self.cc= CardConnector(self, _logger.getEffectiveLevel())
-
- def __repr__(self):
- return ''
-
- def is_pairable(self):
- return True
-
- def close(self):
- _logger.info(f"[SatochipClient] close()")#debugSatochip
- self.cc.card_disconnect()
- self.cc.cardmonitor.deleteObserver(self.cc.cardobserver)
-
- def timeout(self, cutoff):
- pass
-
- def is_initialized(self):
- # TODO - currently set to true #debugSatochip
- return True
-
- def label(self):
- # TODO - currently empty #debugSatochip
- return ""
-
- def has_usable_connection_with_device(self):
- _logger.info(f"[SatochipClient] has_usable_connection_with_device()")#debugSatochip
- try:
- (response, sw1, sw2)=self.cc.card_select() #TODO: something else?
- except SWException as e:
- _logger.exception(f"Exception: {str(e)}")
- return False
- return True
-
- def get_xpub(self, bip32_path, xtype):
- assert xtype in SatochipPlugin.SUPPORTED_XTYPES
-
- # bip32_path is of the form 44'/0'/1'
- _logger.info(f"[SatochipClient] get_xpub(): bip32_path={bip32_path}")#debugSatochip
- (depth, bytepath)= bip32path2bytes(bip32_path)
- (childkey, childchaincode)= self.cc.card_bip32_get_extendedkey(bytepath)
- if depth == 0: #masterkey
- fingerprint= bytes([0,0,0,0])
- child_number= bytes([0,0,0,0])
- else: #get parent info
- (parentkey, parentchaincode)= self.cc.card_bip32_get_extendedkey(bytepath[0:-4])
- fingerprint= hash_160(parentkey.get_public_key_bytes(compressed=True))[0:4]
- child_number= bytepath[-4:]
- xpub= BIP32Node(xtype=xtype,
- eckey=childkey,
- chaincode=childchaincode,
- depth=depth,
- fingerprint=fingerprint,
- child_number=child_number).to_xpub()
- _logger.info(f"[SatochipClient] get_xpub(): xpub={str(xpub)}")#debugSatochip
- return xpub
-
- def ping_check(self):
- #check connection is working
- try:
- print('ping_check')#debug
- #atr= self.cc.card_get_ATR()
- except Exception as e:
- _logger.exception(f"Exception: {str(e)}")
- raise RuntimeError("Communication issue with Satochip")
-
- def request(self, request_type, *args):
- print('[SatochipClient] client request: '+ str(request_type))
-
- if (request_type=='update_status'):
- reply = self.handler.update_status(*args)
- return reply
- elif (request_type=='show_error'):
- reply = self.handler.show_error(*args)
- return reply
- elif (request_type=='show_message'):
- reply = self.handler.show_message(*args)
- return reply
- else:
- reply = self.handler.show_error('Unknown request: '+str(request_type))
- return reply
-
- # try:
- # method_to_call = getattr(self.handler, request_type)
- # print('Type of method_to_call: '+ str(type(method_to_call)))
- # print('method_to_call: '+ str(method_to_call))
- # reply = method_to_call(*args)
- # return reply
- # except Exception as e:
- # _logger.exception(f"Exception: {str(e)}")
- # raise RuntimeError("GUI exception")
-
- def PIN_dialog(self, msg):
- while True:
- password = self.handler.get_passphrase(msg, False)
- if password is None:
- return False, None
- if len(password) < 4:
- msg = _("PIN must have at least 4 characters.") + \
- "\n\n" + _("Enter PIN:")
- elif len(password) > 64:
- msg = _("PIN must have less than 64 characters.") + \
- "\n\n" + _("Enter PIN:")
- else:
- password = password.encode('utf8')
- return True, password
-
- def PIN_setup_dialog(self, msg, msg_confirm, msg_error):
- while(True):
- (is_PIN, pin)= self.PIN_dialog(msg)
- if not is_PIN:
- #return (False, None)
- raise RuntimeError(('A PIN code is required to initialize the Satochip!'))
- (is_PIN, pin_confirm)= self.PIN_dialog(msg_confirm)
- if not is_PIN:
- #return (False, None)
- raise RuntimeError(('A PIN confirmation is required to initialize the Satochip!'))
- if (pin != pin_confirm):
- self.request('show_error', msg_error)
- else:
- return (is_PIN, pin)
-
- def PIN_change_dialog(self, msg_oldpin, msg_newpin, msg_confirm, msg_error, msg_cancel):
- #old pin
- (is_PIN, oldpin)= self.PIN_dialog(msg_oldpin)
- if (not is_PIN):
- self.request('show_message', msg_cancel)
- return (False, None, None)
-
- # new pin
- while (True):
- (is_PIN, newpin)= self.PIN_dialog(msg_newpin)
- if (not is_PIN):
- self.request('show_message', msg_cancel)
- return (False, None, None)
- (is_PIN, pin_confirm)= self.PIN_dialog(msg_confirm)
- if (not is_PIN):
- self.request('show_message', msg_cancel)
- return (False, None, None)
- if (newpin != pin_confirm):
- self.request('show_error', msg_error)
- else:
- return (True, oldpin, newpin)
-
-class Satochip_KeyStore(Hardware_KeyStore):
- hw_type = 'satochip'
- device = 'Satochip'
-
- def __init__(self, d):
- Hardware_KeyStore.__init__(self, d)
- #_logger.info(f"[Satochip_KeyStore] __init__(): xpub:{str(d.get('xpub'))}")#debugSatochip
- #_logger.info(f"[Satochip_KeyStore] __init__(): derivation:{str(d.get('derivation'))}")#debugSatochip
- self.force_watching_only = False
- self.ux_busy = False
-
- def dump(self):
- # our additions to the stored data about keystore -- only during creation?
- d = Hardware_KeyStore.dump(self)
- return d
-
- def get_derivation(self):
- return self.derivation
-
- def get_client(self):
- # called when user tries to do something like view address, sign something.
- # - not called during probing/setup
- rv = self.plugin.get_client(self)
- return rv
-
- def give_error(self, message, clear_client=False):
- _logger.info(message)
- if not self.ux_busy:
- self.handler.show_error(message)
- else:
- self.ux_busy = False
- if clear_client:
- self.client = None
- raise Exception(message)
-
- def decrypt_message(self, pubkey, message, password):
- raise RuntimeError(_('Encryption and decryption are currently not supported for {}').format(self.device))
-
- def sign_message(self, sequence, message, password):
- message_byte = message.encode('utf8')
- message_hash = hashlib.sha256(message_byte).hexdigest().upper()
- client = self.get_client()
- address_path = self.get_derivation()[2:] + "/%d/%d"%sequence
- _logger.info(f"[Satochip_KeyStore] sign_message: path: {address_path}")
- self.handler.show_message("Signing message ...\r\nMessage hash: "+message_hash)
- # check if 2FA is required
- hmac=b''
- if (client.cc.needs_2FA==None):
- (response, sw1, sw2, d)=client.cc.card_get_status()
- if client.cc.needs_2FA:
- # challenge based on sha256(btcheader+msg)
- # format & encrypt msg
- import json
- msg= {'action':"sign_msg", 'msg':message, 'alt':"Litecoin"}
- msg= json.dumps(msg)
- (id_2FA, msg_out)= client.cc.card_crypt_transaction_2FA(msg, True)
- d={}
- d['msg_encrypt']= msg_out
- d['id_2FA']= id_2FA
- # _logger.info("encrypted message: "+msg_out)
- _logger.info("id_2FA: "+id_2FA)
-
- #do challenge-response with 2FA device...
- self.handler.show_message('2FA request sent! Approve or reject request on your second device.')
- Satochip2FA.do_challenge_response(d)
- # decrypt and parse reply to extract challenge response
- try:
- reply_encrypt= d['reply_encrypt']
- except Exception as e:
- self.give_error("No response received from 2FA.\nPlease ensure that the Satochip-2FA plugin is enabled in Tools>Optional Features", True)
- reply_decrypt= client.cc.card_crypt_transaction_2FA(reply_encrypt, False)
- _logger.info("challenge:response= "+ reply_decrypt)
- reply_decrypt= reply_decrypt.split(":")
- chalresponse=reply_decrypt[1]
- hmac= bytes.fromhex(chalresponse)
- try:
- #path= self.get_derivation() + ("/%d/%d" % sequence)
- keynbr= 0xFF #for extended key
- (depth, bytepath)= bip32path2bytes(address_path)
- (pubkey, chaincode)=client.cc.card_bip32_get_extendedkey(bytepath)
- #(response2, sw1, sw2) = client.cc.card_sign_message(keynbr, message_byte, hmac)
- # if (sw1!=0x90 or sw2!=0x00):
- # _logger.info("[satochip] SatochipPlugin: error during sign_message(): sw12="+hex(sw1)+" "+hex(sw2))#debugSatochip
- # compsig=b''
- # self.handler.show_error(_("Wrong signature!\nThe 2FA device may have rejected the action."))
- # else:
- # compsig=client.parser.parse_message_signature(response2, message_byte, pubkey)
- (response2, sw1, sw2, compsig) = client.cc.card_sign_message(keynbr, pubkey, message_byte, hmac, altcoin='Litecoin')
- if (compsig==b''):
- self.handler.show_error(_("Wrong signature!\nThe 2FA device may have rejected the action."))
-
- except Exception as e:
- self.give_error(e, True)
- finally:
- self.handler.finished()
- return compsig
-
- def sign_transaction(self, tx, password):
- _logger.info(f"[Satochip_KeyStore] sign_transaction(): tx: {str(tx)}") #debugSatochip
- client = self.get_client()
- segwitTransaction = False
-
- # outputs
- txOutputs= ''.join(tx.serialize_output(o) for o in tx.outputs())
- hashOutputs = bh2u(sha256d(bfh(txOutputs)))
- txOutputs = var_int(len(tx.outputs()))+txOutputs
- _logger.info(f"[Satochip_KeyStore] sign_transaction(): hashOutputs= {hashOutputs}") #debugSatochip
- _logger.info(f"[Satochip_KeyStore] sign_transaction(): outputs= {txOutputs}") #debugSatochip
-
- # Fetch inputs of the transaction to sign
- derivations = self.get_tx_derivations(tx)
- for i,txin in enumerate(tx.inputs()):
- _logger.info(f"[Satochip_KeyStore] sign_transaction(): input= {str(i)} - input[type]: {txin['type']}") #debugSatochip
- if txin['type'] == 'coinbase':
- self.give_error("Coinbase not supported") # should never happen
-
- if txin['type'] in ['p2sh']:
- p2shTransaction = True
-
- if txin['type'] in ['p2wpkh-p2sh', 'p2wsh-p2sh']:
- segwitTransaction = True
-
- if txin['type'] in ['p2wpkh', 'p2wsh']:
- segwitTransaction = True
-
- pubkeys, x_pubkeys = tx.get_sorted_pubkeys(txin)
- for j, x_pubkey in enumerate(x_pubkeys):
- _logger.info(f"[Satochip_KeyStore] sign_transaction(): forforloop: j= {str(j)}") #debugSatochip
- if tx.is_txin_complete(txin):
- break
-
- if x_pubkey in derivations:
- signingPos = j
- s = derivations.get(x_pubkey)
- address_path = "%s/%d/%d" % (self.get_derivation()[2:], s[0], s[1])
-
- # get corresponing extended key
- (depth, bytepath)= bip32path2bytes(address_path)
- (key, chaincode)=client.cc.card_bip32_get_extendedkey(bytepath)
-
- # parse tx
- pre_tx_hex= tx.serialize_preimage(i)
- pre_tx= bytes.fromhex(pre_tx_hex)# hex representation => converted to bytes
- pre_hash = sha256d(bfh(pre_tx_hex))
- pre_hash_hex= pre_hash.hex()
- _logger.info(f"[Satochip_KeyStore] sign_transaction(): pre_tx_hex= {pre_tx_hex}") #debugSatochip
- _logger.info(f"[Satochip_KeyStore] sign_transaction(): pre_hash= {pre_hash_hex}") #debugSatochip
- # (response, sw1, sw2) = client.cc.card_parse_transaction(pre_tx, segwitTransaction)
- # (tx_hash, needs_2fa)= client.parser.parse_parse_transaction(response)
- (response, sw1, sw2, tx_hash, needs_2fa) = client.cc.card_parse_transaction(pre_tx, segwitTransaction)
- tx_hash_hex= bytearray(tx_hash).hex()
- if pre_hash_hex!= tx_hash_hex:
- raise RuntimeError("[Satochip_KeyStore] Tx preimage mismatch: {pre_hash_hex} vs {tx_hash_hex}")
-
- # sign tx
- keynbr= 0xFF #for extended key
- if needs_2fa:
- # format & encrypt msg
- import json
- coin_type= 2 #see https://github.com/satoshilabs/slips/blob/master/slip-0044.md
- test_net= constants.net.TESTNET
- if segwitTransaction:
- msg= {'tx':pre_tx_hex, 'ct':coin_type, 'tn':test_net, 'sw':segwitTransaction, 'txo':txOutputs, 'ty':txin['type']}
- else:
- msg= {'tx':pre_tx_hex, 'ct':coin_type, 'tn':test_net, 'sw':segwitTransaction}
- msg= json.dumps(msg)
- (id_2FA, msg_out)= client.cc.card_crypt_transaction_2FA(msg, True)
- d={}
- d['msg_encrypt']= msg_out
- d['id_2FA']= id_2FA
- #_logger.info(f"encrypted message: {msg_out}")
- #_logger.info(f"id_2FA: {id_2FA}")
-
- #do challenge-response with 2FA device...
- client.handler.show_message('2FA request sent! Approve or reject request on your second device.')
- Satochip2FA.do_challenge_response(d)
- # decrypt and parse reply to extract challenge response
- try:
- reply_encrypt= d['reply_encrypt']
- except Exception as e:
- self.give_error("No response received from 2FA.\nPlease ensure that the Satochip-2FA plugin is enabled in Tools>Optional Features", True)
- if reply_encrypt is None:
- #todo: abort tx
- break
- reply_decrypt= client.cc.card_crypt_transaction_2FA(reply_encrypt, False)
- _logger.info(f"[Satochip_KeyStore] sign_transaction(): challenge:response= {reply_decrypt}")
- reply_decrypt= reply_decrypt.split(":")
- rep_pre_hash_hex= reply_decrypt[0][0:64]
- if rep_pre_hash_hex!= pre_hash_hex:
- #todo: abort tx or retry?
- _logger.info("Abort transaction: tx mismatch: "+rep_pre_hash_hex+" != "+pre_hash_hex)
- break
- chalresponse=reply_decrypt[1]
- if chalresponse=="00"*20:
- #todo: abort tx
- _logger.info("Transaction rejected by user")
- break
- chalresponse= list(bytes.fromhex(chalresponse))
- else:
- chalresponse= None
- (tx_sig, sw1, sw2) = client.cc.card_sign_transaction(keynbr, tx_hash, chalresponse)
- #_logger.info(f"sign_transaction(): sig= {bytearray(tx_sig).hex()}") #debugSatochip
- #todo: check sw1sw2 for error (0x9c0b if wrong challenge-response)
- # enforce low-S signature (BIP 62)
- tx_sig = bytearray(tx_sig)
- r,s= get_r_and_s_from_der_sig(tx_sig)
- if s > CURVE_ORDER//2:
- s = CURVE_ORDER - s
- tx_sig=der_sig_from_r_and_s(r, s)
- #update tx with signature
- tx_sig = tx_sig.hex()+'01'
- tx.add_signature_to_txin(i,j,tx_sig)
- break
- else:
- self.give_error("No matching x_key for sign_transaction") # should never happen
-
- _logger.info(f"Tx is complete: {str(tx.is_complete())}")
- tx.raw = tx.serialize()
- return
-
- def show_address(self, sequence, txin_type):
- _logger.info(f'[Satochip_KeyStore] show_address(): todo!')
- return
-
-
-class SatochipPlugin(HW_PluginBase):
- libraries_available= True
- minimum_library = (0, 0, 0)
- keystore_class= Satochip_KeyStore
- DEVICE_IDS= [
- (SATOCHIP_VID, SATOCHIP_PID)
- ]
- SUPPORTED_XTYPES = ('standard', 'p2wpkh-p2sh', 'p2wpkh', 'p2wsh-p2sh', 'p2wsh')
-
- def __init__(self, parent, config, name):
-
- _logger.info(f"[SatochipPlugin] init()")#debugSatochip
- HW_PluginBase.__init__(self, parent, config, name)
-
- self.device_manager().register_enumerate_func(self.detect_smartcard_reader)
-
- def get_library_version(self):
- return '0.0.1'
-
- def detect_smartcard_reader(self):
- _logger.info(f"[SatochipPlugin] detect_smartcard_reader")#debugSatochip
- self.cardtype = AnyCardType()
- try:
- cardrequest = CardRequest(timeout=0.1, cardType=self.cardtype)
- cardservice = cardrequest.waitforcard()
- return [Device(path="/satochip",
- interface_number=-1,
- id_="/satochip",
- product_key=(SATOCHIP_VID,SATOCHIP_PID),
- usage_page=0,
- transport_ui_string='ccid')]
- except CardRequestTimeoutException:
- _logger.info(f'time-out: no card found')
- return []
- except Exception as exc:
- _logger.info(f"Error during connection:{str(exc)}")
- return []
- return []
-
-
- def create_client(self, device, handler):
- _logger.info(f"[SatochipPlugin] create_client()")#debugSatochip
-
- if handler:
- self.handler = handler
-
- try:
- rv = SatochipClient(self, handler)
- return rv
- except Exception as e:
- _logger.exception(f"[SatochipPlugin] create_client() exception: {str(e)}")
- return None
-
- def setup_device(self, device_info, wizard, purpose):
- _logger.info(f"[SatochipPlugin] setup_device()")#debugSatochip
-
- devmgr = self.device_manager()
- device_id = device_info.device.id_
- client = devmgr.client_by_id(device_id)
- if client is None:
- raise Exception(_('Failed to create a client for this device.') + '\n' +
- _('Make sure it is in the correct state.'))
- client.handler = self.create_handler(wizard)
-
- # check setup
- while(True):
- (response, sw1, sw2, d)=client.cc.card_get_status()
-
- # check version
- if (client.cc.setup_done):
- v_supported= SATOCHIP_PROTOCOL_VERSION
- v_applet= d["protocol_version"]
- _logger.info(f"[SatochipPlugin] setup_device(): Satochip version={hex(v_applet)} Electrum supported version= {hex(v_supported)}")#debugSatochip
- if (v_supported not multisig, must be bip32
- if type(wallet) is not Standard_Wallet:
- keystore.handler.show_error(_('This function is only available for standard wallets when using {}.').format(self.device))
- return
-
- sequence = wallet.get_address_index(address)
- txin_type = wallet.get_txin_type(address)
- keystore.show_address(sequence, txin_type)
-
- # create/restore seed during satochip initialization
- def choose_seed(self, wizard):
- title = _('Create or restore')
- message = _('Do you want to create a new seed, or to restore a wallet using an existing seed?')
- choices = [
- ('create_seed', _('Create a new seed')),
- ('restore_from_seed', _('I already have a seed')),
- ]
- wizard.choice_dialog(title=title, message=message, choices=choices, run_next=wizard.run)
- #create seed
- def create_seed(self, wizard):
- wizard.seed_type = 'standard'
- wizard.opt_bip39 = False
- seed = Mnemonic('en').make_seed(wizard.seed_type)
- f = lambda x: self.request_passphrase(wizard, seed, x)
- wizard.show_seed_dialog(run_next=f, seed_text=seed)
-
- def request_passphrase(self, wizard, seed, opt_passphrase):
- if opt_passphrase:
- f = lambda x: self.confirm_seed(wizard, seed, x)
- wizard.passphrase_dialog(run_next=f)
- else:
- wizard.run('confirm_seed', seed, '')
-
- def confirm_seed(self, wizard, seed, passphrase):
- f = lambda x: self.confirm_passphrase(wizard, seed, passphrase)
- wizard.confirm_seed_dialog(run_next=f, test=lambda x: x==seed)
-
- def confirm_passphrase(self, wizard, seed, passphrase):
- f = lambda x: self.derive_bip32_seed(seed, x)
- if passphrase:
- title = _('Confirm Seed Extension')
- message = '\n'.join([
- _('Your seed extension must be saved together with your seed.'),
- _('Please type it here.'),
- ])
- wizard.line_dialog(run_next=f, title=title, message=message, default='', test=lambda x: x==passphrase)
- else:
- f('')
-
- def derive_bip32_seed(self, seed, passphrase):
- self.bip32_seed= Mnemonic('en').mnemonic_to_seed(seed, passphrase)
-
- #restore from seed
- def restore_from_seed(self, wizard):
- wizard.opt_bip39 = True
- wizard.opt_ext = True
- test = mnemonic.is_seed
- f= lambda seed, is_bip39, is_ext: self.on_restore_seed(wizard, seed, is_bip39, is_ext)
- wizard.restore_seed_dialog(run_next=f, test=test)
-
- def on_restore_seed(self, wizard, seed, is_bip39, is_ext):
- wizard.seed_type = 'bip39' if is_bip39 else mnemonic.seed_type(seed)
- if wizard.seed_type == 'bip39':
- f = lambda passphrase: self.derive_bip39_seed(seed, passphrase)
- wizard.passphrase_dialog(run_next=f, is_restoring=True) if is_ext else f('')
- elif wizard.seed_type in ['standard', 'segwit']:
- f = lambda passphrase: self.derive_bip32_seed(seed, passphrase)
- wizard.passphrase_dialog(run_next=f, is_restoring=True) if is_ext else f('')
- elif wizard.seed_type == 'old':
- raise Exception('Unsupported seed type', wizard.seed_type)
- elif mnemonic.is_any_2fa_seed_type(wizard.seed_type):
- raise Exception('Unsupported seed type', wizard.seed_type)
- else:
- raise Exception('Unknown seed type', wizard.seed_type)
-
- def derive_bip39_seed(self, seed, passphrase):
- self.bip32_seed=bip39_to_seed(seed, passphrase)
+from os import urandom
+import hashlib
+
+#electrum
+from electrum_ltc import mnemonic
+from electrum_ltc import constants
+from electrum_ltc.bitcoin import TYPE_ADDRESS, int_to_hex, var_int
+from electrum_ltc.i18n import _
+from electrum_ltc.plugin import BasePlugin, Device
+from electrum_ltc.keystore import Hardware_KeyStore, bip39_to_seed
+from electrum_ltc.transaction import Transaction
+from electrum_ltc.wallet import Standard_Wallet
+from electrum_ltc.util import bfh, bh2u, versiontuple
+from electrum_ltc.base_wizard import ScriptTypeNotSupported
+from electrum_ltc.crypto import hash_160, sha256d
+from electrum_ltc.ecc import CURVE_ORDER, der_sig_from_r_and_s, get_r_and_s_from_der_sig, ECPubkey
+from electrum_ltc.bip32 import BIP32Node, convert_bip32_path_to_list_of_uint32, convert_bip32_intpath_to_strpath
+from electrum_ltc.logging import get_logger
+from electrum_ltc.gui.qt.qrcodewidget import QRCodeWidget, QRDialog
+
+from ..hw_wallet import HW_PluginBase, HardwareClientBase
+
+#pysatochip
+from pysatochip.CardConnector import CardConnector, UninitializedSeedError
+from pysatochip.JCconstants import JCconstants
+from pysatochip.TxParser import TxParser
+from pysatochip.Satochip2FA import Satochip2FA
+from pysatochip.version import SATOCHIP_PROTOCOL_MAJOR_VERSION, SATOCHIP_PROTOCOL_MINOR_VERSION, SATOCHIP_PROTOCOL_VERSION
+
+#pyscard
+from smartcard.sw.SWExceptions import SWException
+from smartcard.Exceptions import CardConnectionException, CardRequestTimeoutException
+from smartcard.CardType import AnyCardType
+from smartcard.CardRequest import CardRequest
+
+_logger = get_logger(__name__)
+
+# version history for the plugin
+SATOCHIP_PLUGIN_REVISION= 'lib0.11.a-plugin0.1'
+
+# debug: smartcard reader ids
+SATOCHIP_VID= 0 #0x096E
+SATOCHIP_PID= 0 #0x0503
+
+MSG_USE_2FA= _("Do you want to use 2-Factor-Authentication (2FA)?\n\nWith 2FA, any transaction must be confirmed on a second device such as your smartphone. First you have to install the Satochip-2FA android app on google play. Then you have to pair your 2FA device with your Satochip by scanning the qr-code on the next screen. \n\nWARNING: be sure to backup a copy of the qr-code in a safe place, in case you have to reinstall the app!")
+
+# def bip32path2bytes(bip32path:str) -> (int, bytes):
+ # splitPath = bip32path.split('/')
+ # splitPath=[x for x in splitPath if x] # removes empty values
+ # if splitPath[0] == 'm':
+ # splitPath = splitPath[1:]
+
+ # bytePath=b''
+ # depth= len(splitPath)
+ # for index in splitPath:
+ # if index.endswith("'"):
+ # bytePath+= pack( ">I", int(index.rstrip("'"))+0x80000000 )
+ # else:
+ # bytePath+=pack( ">I", int(index) )
+
+ # return (depth, bytePath)
+
+def bip32path2bytes(bip32path:str) -> (int, bytes):
+ intPath= convert_bip32_path_to_list_of_uint32(bip32path)
+ depth= len(intPath)
+ bytePath=b''
+ for index in intPath:
+ bytePath+= index.to_bytes(4, byteorder='big', signed=False)
+ return (depth, bytePath)
+
+class SatochipClient(HardwareClientBase):
+ def __init__(self, plugin: HW_PluginBase, handler):
+ HardwareClientBase.__init__(self, plugin=plugin)
+ _logger.info(f"[SatochipClient] __init__()")#debugSatochip
+ self._soft_device_id = None
+ self.device = plugin.device
+ self.handler = handler
+ #self.parser= CardDataParser()
+ self.cc= CardConnector(self, _logger.getEffectiveLevel())
+
+ def __repr__(self):
+ return ''
+
+ def is_pairable(self):
+ return True
+
+ def close(self):
+ _logger.info(f"close()")
+ self.cc.card_disconnect()
+ self.cc.cardmonitor.deleteObserver(self.cc.cardobserver)
+
+ def timeout(self, cutoff):
+ pass
+
+ def is_initialized(self):
+ # TODO - currently set to true #debugSatochip
+ return True
+
+ def get_soft_device_id(self):
+ return self._soft_device_id
+
+ def label(self):
+ # TODO - currently empty #debugSatochip
+ return ""
+
+ def device_model_name(self):
+ return "Satochip"
+
+ def has_usable_connection_with_device(self):
+ _logger.info(f"has_usable_connection_with_device()")#debugSatochip
+ try:
+ atr= self.cc.card_get_ATR() # (response, sw1, sw2)= self.cc.card_select() #TODO: something else? get ATR?
+ _logger.info("Card ATR: " + bytes(atr).hex() )
+ except Exception as e: #except SWException as e:
+ _logger.exception(f"Exception in has_usable_connection_with_device: {str(e)}")
+ return False
+ return True
+
+ def get_xpub(self, bip32_path, xtype):
+ assert xtype in SatochipPlugin.SUPPORTED_XTYPES
+
+ # needs PIN
+ self.cc.card_verify_PIN()
+
+ # bip32_path is of the form 44'/0'/1'
+ _logger.info(f"[SatochipClient] get_xpub(): bip32_path={bip32_path}")#debugSatochip
+ (depth, bytepath)= bip32path2bytes(bip32_path)
+ (childkey, childchaincode)= self.cc.card_bip32_get_extendedkey(bytepath)
+ if depth == 0: #masterkey
+ fingerprint= bytes([0,0,0,0])
+ child_number= bytes([0,0,0,0])
+ else: #get parent info
+ (parentkey, parentchaincode)= self.cc.card_bip32_get_extendedkey(bytepath[0:-4])
+ fingerprint= hash_160(parentkey.get_public_key_bytes(compressed=True))[0:4]
+ child_number= bytepath[-4:]
+ xpub= BIP32Node(xtype=xtype,
+ eckey=childkey,
+ chaincode=childchaincode,
+ depth=depth,
+ fingerprint=fingerprint,
+ child_number=child_number).to_xpub()
+ _logger.info(f"[SatochipClient] get_xpub(): xpub={str(xpub)}")#debugSatochip
+ return xpub
+
+ def ping_check(self):
+ #check connection is working
+ try:
+ print('ping_check')#debug
+ #atr= self.cc.card_get_ATR()
+ except Exception as e:
+ _logger.exception(f"Exception: {str(e)}")
+ raise RuntimeError("Communication issue with Satochip")
+
+ def request(self, request_type, *args):
+ _logger.info('[SatochipClient] client request: '+ str(request_type))#debugSatochip
+
+ if self.handler is not None:
+ if (request_type=='update_status'):
+ reply = self.handler.update_status(*args)
+ return reply
+ elif (request_type=='show_error'):
+ reply = self.handler.show_error(*args)
+ return reply
+ elif (request_type=='show_message'):
+ reply = self.handler.show_message(*args)
+ return reply
+ else:
+ reply = self.handler.show_error('Unknown request: '+str(request_type))
+ return reply
+ else:
+ _logger.info('[SatochipClient] self.handler is None! ')#debugSatochip
+ return None
+ # try:
+ # method_to_call = getattr(self.handler, request_type)
+ # print('Type of method_to_call: '+ str(type(method_to_call)))
+ # print('method_to_call: '+ str(method_to_call))
+ # reply = method_to_call(*args)
+ # return reply
+ # except Exception as e:
+ # _logger.exception(f"Exception: {str(e)}")
+ # raise RuntimeError("GUI exception")
+
+ def PIN_dialog(self, msg):
+ while True:
+ password = self.handler.get_passphrase(msg, False)
+ if password is None:
+ return False, None
+ if len(password) < 4:
+ msg = _("PIN must have at least 4 characters.") + \
+ "\n\n" + _("Enter PIN:")
+ elif len(password) > 16:
+ msg = _("PIN must have less than 16 characters.") + \
+ "\n\n" + _("Enter PIN:")
+ else:
+ password = password.encode('utf8')
+ return True, password
+
+ def PIN_setup_dialog(self, msg, msg_confirm, msg_error):
+ while(True):
+ (is_PIN, pin)= self.PIN_dialog(msg)
+ if not is_PIN:
+ #return (False, None)
+ raise RuntimeError(('A PIN code is required to initialize the Satochip!'))
+ (is_PIN, pin_confirm)= self.PIN_dialog(msg_confirm)
+ if not is_PIN:
+ #return (False, None)
+ raise RuntimeError(('A PIN confirmation is required to initialize the Satochip!'))
+ if (pin != pin_confirm):
+ self.request('show_error', msg_error)
+ else:
+ return (is_PIN, pin)
+
+ def PIN_change_dialog(self, msg_oldpin, msg_newpin, msg_confirm, msg_error, msg_cancel):
+ #old pin
+ (is_PIN, oldpin)= self.PIN_dialog(msg_oldpin)
+ if (not is_PIN):
+ self.request('show_message', msg_cancel)
+ return (False, None, None)
+
+ # new pin
+ while (True):
+ (is_PIN, newpin)= self.PIN_dialog(msg_newpin)
+ if (not is_PIN):
+ self.request('show_message', msg_cancel)
+ return (False, None, None)
+ (is_PIN, pin_confirm)= self.PIN_dialog(msg_confirm)
+ if (not is_PIN):
+ self.request('show_message', msg_cancel)
+ return (False, None, None)
+ if (newpin != pin_confirm):
+ self.request('show_error', msg_error)
+ else:
+ return (True, oldpin, newpin)
+
+class Satochip_KeyStore(Hardware_KeyStore):
+ hw_type = 'satochip'
+ device = 'Satochip'
+ plugin: 'SatochipPlugin'
+
+ def __init__(self, d):
+ Hardware_KeyStore.__init__(self, d)
+ #_logger.info(f"[Satochip_KeyStore] __init__(): xpub:{str(d.get('xpub'))}")#debugSatochip
+ #_logger.info(f"[Satochip_KeyStore] __init__(): derivation:{str(d.get('derivation'))}")#debugSatochip
+ self.force_watching_only = False
+ self.ux_busy = False
+
+ def dump(self):
+ # our additions to the stored data about keystore -- only during creation?
+ d = Hardware_KeyStore.dump(self)
+ return d
+
+ def get_derivation(self):
+ return self.derivation
+
+
+ def get_client(self):
+ # called when user tries to do something like view address, sign something.
+ # - not called during probing/setup
+ rv = self.plugin.get_client(self)
+ return rv
+
+ def give_error(self, message, clear_client=False):
+ _logger.info(message)
+ if not self.ux_busy:
+ self.handler.show_error(message)
+ else:
+ self.ux_busy = False
+ if clear_client:
+ self.client = None
+ raise Exception(message)
+
+ def decrypt_message(self, pubkey, message, password):
+ raise RuntimeError(_('Encryption and decryption are currently not supported for {}').format(self.device))
+
+ def sign_message(self, sequence, message, password):
+ message_byte = message.encode('utf8')
+ message_hash = hashlib.sha256(message_byte).hexdigest().upper()
+ client = self.get_client()
+ address_path = self.get_derivation_prefix() + "/%d/%d"%sequence #self.get_derivation()[2:] + "/%d/%d"%sequence
+ _logger.info(f"[Satochip_KeyStore] sign_message: path: {address_path}")
+ self.handler.show_message("Signing message ...\r\nMessage hash: "+message_hash)
+ # check if 2FA is required
+ hmac=b''
+ if (client.cc.needs_2FA==None):
+ (response, sw1, sw2, d)=client.cc.card_get_status()
+ if client.cc.needs_2FA:
+ # challenge based on sha256(btcheader+msg)
+ # format & encrypt msg
+ import json
+ msg= {'action':"sign_msg", 'msg':message, 'alt':"Litecoin"}
+ msg= json.dumps(msg)
+ (id_2FA, msg_out)= client.cc.card_crypt_transaction_2FA(msg, True)
+
+ d={}
+ d['msg_encrypt']= msg_out
+ d['id_2FA']= id_2FA
+ # _logger.info("encrypted message: "+msg_out)
+ _logger.info("id_2FA: "+id_2FA)
+
+ #do challenge-response with 2FA device...
+ self.handler.show_message('2FA request sent! Approve or reject request on your second device.')
+ Satochip2FA.do_challenge_response(d)
+ # decrypt and parse reply to extract challenge response
+ try:
+ reply_encrypt= d['reply_encrypt']
+ except Exception as e:
+ self.give_error("No response received from 2FA.\nPlease ensure that the Satochip-2FA plugin is enabled in Tools>Optional Features", True)
+ reply_decrypt= client.cc.card_crypt_transaction_2FA(reply_encrypt, False)
+ _logger.info("challenge:response= "+ reply_decrypt)
+ reply_decrypt= reply_decrypt.split(":")
+ chalresponse=reply_decrypt[1]
+ hmac= bytes.fromhex(chalresponse)
+ try:
+ keynbr= 0xFF #for extended key
+ (depth, bytepath)= bip32path2bytes(address_path)
+ (pubkey, chaincode)=client.cc.card_bip32_get_extendedkey(bytepath)
+ (response2, sw1, sw2, compsig) = client.cc.card_sign_message(keynbr, pubkey, message_byte, hmac, altcoin='Litecoin')
+ if (compsig==b''):
+ self.handler.show_error(_("Wrong signature!\nThe 2FA device may have rejected the action."))
+
+ except Exception as e:
+ self.give_error(e, True)
+ finally:
+ self.handler.finished()
+ return compsig
+
+ def sign_transaction(self, tx, password):
+ _logger.info(f"In sign_transaction(): tx: {str(tx)}") #debugSatochip
+ client = self.get_client()
+ segwitTransaction = False
+
+ # outputs
+ txOutputs = var_int(len(tx.outputs()))
+ for o in tx.outputs():
+ txOutputs += int_to_hex(o.value, 8)
+ script = o.scriptpubkey.hex()
+ txOutputs += var_int(len(script)//2)
+ txOutputs += script
+ #txOutputs = bfh(txOutputs)
+ hashOutputs = bh2u(sha256d(bfh(txOutputs)))
+ _logger.info(f"In sign_transaction(): hashOutputs= {hashOutputs}") #debugSatochip
+ _logger.info(f"In sign_transaction(): outputs= {txOutputs}") #debugSatochip
+
+ # Fetch inputs of the transaction to sign
+ for i,txin in enumerate(tx.inputs()):
+
+ if tx.is_complete():
+ break
+
+ _logger.info(f"In sign_transaction(): input= {str(i)} - input[type]: {txin.script_type}") #debugSatochip
+ if txin.is_coinbase_input():
+ self.give_error("Coinbase not supported") # should never happen
+
+ if txin.script_type in ['p2sh']:
+ p2shTransaction = True
+
+ if txin.script_type in ['p2wpkh', 'p2wsh', 'p2wpkh-p2sh', 'p2wsh-p2sh']:
+ segwitTransaction = True
+
+ my_pubkey, inputPath = self.find_my_pubkey_in_txinout(txin)
+ # _logger.info(f"In sign_transaction(): txin.json: {str(txin.to_json())}") #type:PartialTxInput #debugSatochip
+ # _logger.info(f"In sign_transaction(): my_pubkey: {str(my_pubkey)} - inputPath: {str(inputPath)}") #debugSatochip
+ if not inputPath:
+ self.give_error("No matching pubkey for sign_transaction") # should never happen
+ inputPath = convert_bip32_intpath_to_strpath(inputPath) #[2:]
+ inputHash = sha256d(bfh(tx.serialize_preimage(i)))
+
+ # get corresponing extended key
+ (depth, bytepath)= bip32path2bytes(inputPath)
+ (key, chaincode)=client.cc.card_bip32_get_extendedkey(bytepath)
+
+ # parse tx
+ pre_tx_hex= tx.serialize_preimage(i)
+ pre_tx= bytes.fromhex(pre_tx_hex)# hex representation => converted to bytes
+ pre_hash = sha256d(bfh(pre_tx_hex))
+ pre_hash_hex= pre_hash.hex()
+ _logger.info(f"[Satochip_KeyStore] sign_transaction(): pre_tx_hex= {pre_tx_hex}") #debugSatochip
+ _logger.info(f"[Satochip_KeyStore] sign_transaction(): pre_hash= {pre_hash_hex}") #debugSatochip
+ (response, sw1, sw2, tx_hash, needs_2fa) = client.cc.card_parse_transaction(pre_tx, segwitTransaction)
+ tx_hash_hex= bytearray(tx_hash).hex()
+ if pre_hash_hex!= tx_hash_hex:
+ raise RuntimeError("[Satochip_KeyStore] Tx preimage mismatch: {pre_hash_hex} vs {tx_hash_hex}")
+
+ #2FA
+ keynbr= 0xFF #for extended key
+ if needs_2fa:
+ # format & encrypt msg
+ import json
+ coin_type= 2 # for litecoin - see https://github.com/satoshilabs/slips/blob/master/slip-0044.md
+ test_net= constants.net.TESTNET # litecoin mainnet or testnet
+ if segwitTransaction:
+ msg= {'tx':pre_tx_hex, 'ct':coin_type, 'tn':test_net, 'sw':segwitTransaction, 'txo':txOutputs, 'ty':txin.script_type} #litecoin
+ else:
+ msg= {'tx':pre_tx_hex, 'ct':coin_type, 'tn':test_net, 'sw':segwitTransaction} #litecoin
+ msg= json.dumps(msg)
+ (id_2FA, msg_out)= client.cc.card_crypt_transaction_2FA(msg, True)
+ d={}
+ d['msg_encrypt']= msg_out
+ d['id_2FA']= id_2FA
+ #_logger.info(f"encrypted message: {msg_out}")
+ #_logger.info(f"id_2FA: {id_2FA}")
+
+ #do challenge-response with 2FA device...
+ client.handler.show_message('2FA request sent! Approve or reject request on your second device.')
+ Satochip2FA.do_challenge_response(d)
+ # decrypt and parse reply to extract challenge response
+ try:
+ reply_encrypt= d['reply_encrypt']
+ except Exception as e:
+ self.give_error("No response received from 2FA.\nPlease ensure that the Satochip-2FA plugin is enabled in Tools>Optional Features", True)
+ if reply_encrypt is None:
+ #todo: abort tx
+ _logger.info("Abort transaction: no reply received from 2FA!")
+ break
+ reply_decrypt= client.cc.card_crypt_transaction_2FA(reply_encrypt, False)
+ _logger.info(f"[Satochip_KeyStore] sign_transaction(): challenge:response= {reply_decrypt}")
+ reply_decrypt= reply_decrypt.split(":")
+ rep_pre_hash_hex= reply_decrypt[0][0:64]
+ if rep_pre_hash_hex!= pre_hash_hex:
+ #todo: abort tx or retry?
+ _logger.info("Abort transaction: tx mismatch: "+rep_pre_hash_hex+" != "+pre_hash_hex)
+ break
+ chalresponse=reply_decrypt[1]
+ if chalresponse=="00"*20:
+ #todo: abort tx
+ _logger.info("Abort transaction: rejected by 2FA!")
+ break
+ chalresponse= list(bytes.fromhex(chalresponse))
+ else:
+ chalresponse= None
+
+ # sign tx
+ (tx_sig, sw1, sw2) = client.cc.card_sign_transaction(keynbr, tx_hash, chalresponse)
+ #_logger.info(f"sign_transaction(): sig= {bytearray(tx_sig).hex()}") #debugSatochip
+ #todo: check sw1sw2 for error (0x9c0b if wrong challenge-response)
+ # enforce low-S signature (BIP 62)
+ tx_sig = bytes(tx_sig) #bytearray(tx_sig)
+ r,s= get_r_and_s_from_der_sig(tx_sig)
+ if s > CURVE_ORDER//2:
+ s = CURVE_ORDER - s
+ tx_sig=der_sig_from_r_and_s(r, s)
+ #update tx with signature
+ tx_sig = tx_sig.hex()+'01'
+ #tx.add_signature_to_txin(i,j,tx_sig)
+ tx.add_signature_to_txin(txin_idx=i,
+ signing_pubkey=my_pubkey.hex(),
+ sig=tx_sig)
+ # end of for loop
+
+ _logger.info(f"Tx is complete: {str(tx.is_complete())}")
+ tx.raw = tx.serialize()
+ return
+
+ def show_address(self, sequence, txin_type):
+ _logger.info(f'[Satochip_KeyStore] show_address(): todo!')
+ return
+
+
+class SatochipPlugin(HW_PluginBase):
+ libraries_available= True
+ minimum_library = (0, 0, 0)
+ keystore_class= Satochip_KeyStore
+ DEVICE_IDS= [
+ (SATOCHIP_VID, SATOCHIP_PID)
+ ]
+ SUPPORTED_XTYPES = ('standard', 'p2wpkh-p2sh', 'p2wpkh', 'p2wsh-p2sh', 'p2wsh')
+
+ def __init__(self, parent, config, name):
+
+ _logger.info(f"[SatochipPlugin] init()")#debugSatochip
+ HW_PluginBase.__init__(self, parent, config, name)
+
+ self.device_manager().register_enumerate_func(self.detect_smartcard_reader)
+
+ def get_library_version(self):
+ return '0.0.1'
+
+ def detect_smartcard_reader(self):
+ _logger.info(f"[SatochipPlugin] detect_smartcard_reader")#debugSatochip
+ self.cardtype = AnyCardType()
+ try:
+ cardrequest = CardRequest(timeout=0.1, cardType=self.cardtype)
+ cardservice = cardrequest.waitforcard()
+ return [Device(path="/satochip",
+ interface_number=-1,
+ id_="/satochip",
+ product_key=(SATOCHIP_VID,SATOCHIP_PID),
+ usage_page=0,
+ transport_ui_string='ccid')]
+ except CardRequestTimeoutException:
+ _logger.info(f'time-out: no card found')
+ return []
+ except Exception as exc:
+ _logger.info(f"Error during connection:{str(exc)}")
+ return []
+ return []
+
+
+ def create_client(self, device, handler):
+ _logger.info(f"[SatochipPlugin] create_client()")#debugSatochip
+
+ if handler:
+ self.handler = handler
+
+ try:
+ rv = SatochipClient(self, handler)
+ return rv
+ except Exception as e:
+ _logger.exception(f"[SatochipPlugin] create_client() exception: {str(e)}")
+ return None
+
+ def setup_device(self, device_info, wizard, purpose):
+ _logger.info(f"[SatochipPlugin] setup_device()")#debugSatochip
+
+ #TODO: use scan_and_create_client_for_device?
+ devmgr = self.device_manager()
+ device_id = device_info.device.id_
+ client = devmgr.client_by_id(device_id)
+ if client is None:
+ raise Exception(_('Failed to create a client for this device.') + '\n' +
+ _('Make sure it is in the correct state.'))
+ client.handler = self.create_handler(wizard)
+
+ # check setup
+ while(client.cc.card_present):
+ (response, sw1, sw2, d)=client.cc.card_get_status()
+
+ # check version
+ if (client.cc.setup_done):
+ v_supported= SATOCHIP_PROTOCOL_VERSION
+ v_applet= d["protocol_version"]
+ _logger.info(f"[SatochipPlugin] setup_device(): Satochip version={hex(v_applet)} Electrum supported version= {hex(v_supported)}")#debugSatochip
+ if (v_supported not multisig, must be bip32
+ if type(wallet) is not Standard_Wallet:
+ keystore.handler.show_error(_('This function is only available for standard wallets when using {}.').format(self.device))
+ return
+
+ sequence = wallet.get_address_index(address)
+ txin_type = wallet.get_txin_type(address)
+ keystore.show_address(sequence, txin_type)
+
+ # create/restore seed during satochip initialization
+ def choose_seed(self, wizard):
+ title = _('Create or restore')
+ message = _('Do you want to create a new seed, or to restore a wallet using an existing seed?')
+ choices = [
+ ('create_seed', _('Create a new BIP39 seed')),
+ ('restore_from_seed', _('I already have a BIP39 seed')),
+ ]
+ wizard.choice_dialog(title=title, message=message, choices=choices, run_next=wizard.run)
+ #create seed
+ def create_seed(self, wizard):
+ wizard.seed_type = 'bip39'
+ wizard.opt_bip39 = True
+ #seed = mnemonic.Mnemonic('en').make_seed(wizard.seed_type) # Electrum seed
+ seed= self.to_bip39_mnemonic(128)
+ f = lambda x: self.request_passphrase(wizard, seed, x)
+ wizard.show_seed_dialog(run_next=f, seed_text=seed)
+
+ def request_passphrase(self, wizard, seed, opt_passphrase):
+ if opt_passphrase:
+ f = lambda x: self.confirm_seed(wizard, seed, x)
+ wizard.passphrase_dialog(run_next=f)
+ else:
+ wizard.run('confirm_seed', seed, '')
+
+ def confirm_seed(self, wizard, seed, passphrase):
+ f = lambda x: self.confirm_passphrase(wizard, seed, passphrase)
+ wizard.confirm_seed_dialog(run_next=f, seed='', test=lambda x: x==seed)
+
+ def confirm_passphrase(self, wizard, seed, passphrase):
+ f = lambda x: self.derive_bip39_seed(seed, x) #f = lambda x: self.derive_bip32_seed(seed, x)
+
+ if passphrase:
+ title = _('Confirm Seed Extension')
+ message = '\n'.join([
+ _('Your seed extension must be saved together with your seed.'),
+ _('Please type it here.'),
+ ])
+ wizard.line_dialog(run_next=f, title=title, message=message, default='', test=lambda x: x==passphrase)
+ else:
+ f('')
+
+
+
+
+ #restore from seed
+ def restore_from_seed(self, wizard):
+ wizard.opt_bip39 = True
+ wizard.opt_ext = True
+ test = mnemonic.is_seed
+ f= lambda seed, is_bip39, is_ext: self.on_restore_seed(wizard, seed, is_bip39, is_ext)
+ wizard.restore_seed_dialog(run_next=f, test=test)
+
+
+ def on_restore_seed(self, wizard, seed, is_bip39, is_ext):
+ wizard.seed_type = 'bip39' if is_bip39 else mnemonic.seed_type(seed)
+ if wizard.seed_type == 'bip39':
+ f = lambda passphrase: self.derive_bip39_seed(seed, passphrase)
+ wizard.passphrase_dialog(run_next=f, is_restoring=True) if is_ext else f('')
+ elif wizard.seed_type in ['standard', 'segwit']:
+ # warning message as Electrum seed on hardware is not standard and incompatible with other hw
+ message= ' '.join([
+ _("You are trying to import an Electrum seed to a Satochip hardware wallet."),
+ _("\n\nElectrum seeds are not compatible with the BIP39 seeds typically used in hardware wallets."),
+ _("This means you may have difficulty to import this seed in another wallet in the future."),
+ _("\n\nProceed with caution! If you are not sure, click on 'Back', enable BIP39 in 'Options' and introduce a BIP39 seed instead."),
+ _("You can also generate a new random BIP39 seed by clicking on 'Back' twice.")
+ ])
+ wizard.confirm_dialog('Warning', message, run_next=lambda x: None)
+ f = lambda passphrase: self.derive_bip32_seed(seed, passphrase)
+ wizard.passphrase_dialog(run_next=f, is_restoring=True) if is_ext else f('')
+ elif wizard.seed_type == 'old':
+ raise Exception('Unsupported seed type', wizard.seed_type)
+ elif mnemonic.is_any_2fa_seed_type(wizard.seed_type):
+ raise Exception('Unsupported seed type', wizard.seed_type)
+ else:
+ raise Exception('Unknown seed type', wizard.seed_type)
+
+ def derive_bip32_seed(self, seed, passphrase):
+ self.bip32_seed= mnemonic.Mnemonic('en').mnemonic_to_seed(seed, passphrase)
+
+ def derive_bip39_seed(self, seed, passphrase):
+ self.bip32_seed=bip39_to_seed(seed, passphrase)
+
+ # based on https://github.com/trezor/python-mnemonic/blob/master/mnemonic/mnemonic.py
+ def to_bip39_mnemonic(self, strength: int) -> str:
+ wordlist = mnemonic.Wordlist.from_file("english.txt")
+ data= urandom(strength // 8)
+ if len(data) not in [16, 20, 24, 28, 32]:
+ raise ValueError(
+ "Data length should be one of the following: [16, 20, 24, 28, 32], but it is not (%d)."
+ % len(data)
+ )
+ h = hashlib.sha256(data).hexdigest()
+ b = (
+ bin(int.from_bytes(data, byteorder="big"))[2:].zfill(len(data) * 8)
+ + bin(int(h, 16))[2:].zfill(256)[: len(data) * 8 // 32]
+ )
+ result = []
+ for i in range(len(b) // 11):
+ idx = int(b[i * 11 : (i + 1) * 11], 2)
+ result.append(wordlist[idx])
+ result_phrase = " ".join(result)
+ return result_phrase
+
+