From 295dc1261264d77b1e5b35800abd9686498932be Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 27 Dec 2017 18:26:50 +0100 Subject: [PATCH 01/11] avoid shorter seeds 'by luck' --- lib/mnemonic.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/mnemonic.py b/lib/mnemonic.py index 10e0e08b2e76..7096e20f64d2 100644 --- a/lib/mnemonic.py +++ b/lib/mnemonic.py @@ -171,7 +171,10 @@ def make_seed(self, seed_type='standard', num_bits=132, custom_entropy=1): n_custom = int(math.ceil(math.log(custom_entropy, 2))) n = max(16, num_bits - n_custom) print_error("make_seed", prefix, "adding %d bits"%n) - my_entropy = ecdsa.util.randrange(pow(2, n)) + my_entropy = 1 + while my_entropy < pow(2, n - bpw): + # try again if seed would not contain enough words + my_entropy = ecdsa.util.randrange(pow(2, n)) nonce = 0 while True: nonce += 1 From 19ba2364b6dfcc71ed135d470903fbe0211e1660 Mon Sep 17 00:00:00 2001 From: Ali Raheem Date: Mon, 1 Jan 2018 20:55:10 +0000 Subject: [PATCH 02/11] Show message sign when signing Allow verification by showing the SHA256 hash of the message to be signed (which is also displayed on Ledger). --- plugins/ledger/ledger.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/ledger/ledger.py b/plugins/ledger/ledger.py index a82cb8442b9d..e6866dc2c0bf 100644 --- a/plugins/ledger/ledger.py +++ b/plugins/ledger/ledger.py @@ -220,10 +220,11 @@ def decrypt_message(self, pubkey, message, password): def sign_message(self, sequence, message, password): self.signing = True message = message.encode('utf8') + message_hash = hashlib.sha256(message).hexdigest() # prompt for the PIN before displaying the dialog if necessary client = self.get_client() address_path = self.get_derivation()[2:] + "/%d/%d"%sequence - self.handler.show_message("Signing message ...") + self.handler.show_message("Signing message ...\r\nMessage hash: "+message_hash) try: info = self.get_client().signMessagePrepare(address_path, message) pin = "" From ed86fee3feed60cd2740562318974e86655a29a4 Mon Sep 17 00:00:00 2001 From: Ali Raheem Date: Mon, 1 Jan 2018 21:23:37 +0000 Subject: [PATCH 03/11] Format of message hash to match Ledger On signing SHA256 of message is shown on Ledger in caps. Make Electrum show in same format to save confusion. --- plugins/ledger/ledger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/ledger/ledger.py b/plugins/ledger/ledger.py index e6866dc2c0bf..e9a0063d36ef 100644 --- a/plugins/ledger/ledger.py +++ b/plugins/ledger/ledger.py @@ -220,7 +220,7 @@ def decrypt_message(self, pubkey, message, password): def sign_message(self, sequence, message, password): self.signing = True message = message.encode('utf8') - message_hash = hashlib.sha256(message).hexdigest() + message_hash = hashlib.sha256(message).hexdigest().upper() # prompt for the PIN before displaying the dialog if necessary client = self.get_client() address_path = self.get_derivation()[2:] + "/%d/%d"%sequence From 0a83b3af224d3c1cb591af8211b2f80b3dca409c Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Mon, 1 Jan 2018 18:03:00 -0500 Subject: [PATCH 04/11] Avoid modifying self.transactions in prepare_for_verifier In python3, the `.keys()` function returns an iterator, not a list, so to get a list that can be iterated over, use `list()` instead to avoid modification of a list while in use. --- lib/wallet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/wallet.py b/lib/wallet.py index bc6dab6d0984..3a3ed8f89174 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -969,7 +969,7 @@ def prepare_for_verifier(self): # if we are on a pruning server, remove unverified transactions with self.lock: vr = list(self.verified_tx.keys()) + list(self.unverified_tx.keys()) - for tx_hash in self.transactions.keys(): + for tx_hash in list(self.transactions): if tx_hash not in vr: self.print_error("removing transaction", tx_hash) self.transactions.pop(tx_hash) From 563aae0dbd6f29c74e18bb0b2ca18ca340f40706 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 4 Jan 2018 03:40:35 +0100 Subject: [PATCH 05/11] fix #3411 --- lib/pem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pem.py b/lib/pem.py index 06b23df09517..40390081b5b5 100644 --- a/lib/pem.py +++ b/lib/pem.py @@ -165,7 +165,7 @@ def _parsePKCS8(_bytes): def _parseSSLeay(bytes): - return _parseASN1PrivateKey(ASN1_Node(str(bytes))) + return _parseASN1PrivateKey(ASN1_Node(bytes)) def bytesToNumber(s): From 99a08f35b20c88ee9979fe5c38ad0c4c8f728c04 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 4 Jan 2018 21:30:06 +0100 Subject: [PATCH 06/11] Clean up messages about msg signing/verification. Allow translations. Test is_mine before txin_type. Show txin_type in msg. --- gui/qt/main_window.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py index ea2fe96d21a9..7aa04b228254 100644 --- a/gui/qt/main_window.py +++ b/gui/qt/main_window.py @@ -2017,24 +2017,26 @@ def show_private_key(self, address, password): d.setLayout(vbox) d.exec_() - msg_sign = ("Signing with an address actually means signing with the corresponding " + msg_sign = _("Signing with an address actually means signing with the corresponding " "private key, and verifying with the corresponding public key. The " "address you have entered does not have a unique public key, so these " - "operations cannot be performed.") + "operations cannot be performed.") + '\n\n' + \ + _('The operation is undefined. Not just in Electrum, but in general.') @protected def do_sign(self, address, message, signature, password): address = address.text().strip() message = message.toPlainText().strip() if not bitcoin.is_address(address): - self.show_message('Invalid Bitcoin address.') + self.show_message(_('Invalid Bitcoin address.')) + return + if not self.wallet.is_mine(address): + self.show_message(_('Address not in wallet.')) return txin_type = self.wallet.get_txin_type(address) if txin_type not in ['p2pkh', 'p2wpkh', 'p2wpkh-p2sh']: - self.show_message('Cannot sign messages with this type of address.' + '\n\n' + self.msg_sign) - return - if not self.wallet.is_mine(address): - self.show_message('Address not in wallet.') + self.show_message(_('Cannot sign messages with this type of address:') + \ + ' ' + txin_type + '\n\n' + self.msg_sign) return task = partial(self.wallet.sign_message, address, message, password) @@ -2046,7 +2048,7 @@ def do_verify(self, address, message, signature): address = address.text().strip() message = message.toPlainText().strip().encode('utf-8') if not bitcoin.is_address(address): - self.show_message('Invalid Bitcoin address.') + self.show_message(_('Invalid Bitcoin address.')) return try: # This can throw on invalid base64 From 6d88eab005c306eb9fda6249cf387656c56a2122 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 4 Jan 2018 22:21:05 +0100 Subject: [PATCH 07/11] tests: added 2fa seed case to test_wallet_vertical.py --- lib/tests/test_wallet_vertical.py | 48 +++++++++++++++++++++++++++--- plugins/trustedcoin/trustedcoin.py | 2 ++ 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/lib/tests/test_wallet_vertical.py b/lib/tests/test_wallet_vertical.py index da8db9edd60f..f909db6d96c8 100644 --- a/lib/tests/test_wallet_vertical.py +++ b/lib/tests/test_wallet_vertical.py @@ -6,8 +6,10 @@ import lib.storage as storage import lib.wallet as wallet +from plugins.trustedcoin import trustedcoin -# TODO: 2fa + +# TODO passphrase/seed_extension class TestWalletKeystoreAddressIntegrity(unittest.TestCase): gap_limit = 1 # make tests run faster @@ -32,12 +34,17 @@ def _create_standard_wallet(self, ks): w.synchronize() return w - def _create_multisig_wallet(self, ks1, ks2): + def _create_multisig_wallet(self, ks1, ks2, ks3=None): + """Creates a 2-of-2 or 2-of-3 multisig wallet.""" store = storage.WalletStorage('if_this_exists_mocking_failed_648151893') - multisig_type = "%dof%d" % (2, 2) - store.put('wallet_type', multisig_type) store.put('x%d/' % 1, ks1.dump()) store.put('x%d/' % 2, ks2.dump()) + if ks3 is None: + multisig_type = "%dof%d" % (2, 2) + else: + multisig_type = "%dof%d" % (2, 3) + store.put('x%d/' % 3, ks3.dump()) + store.put('wallet_type', multisig_type) store.put('gap_limit', self.gap_limit) w = wallet.Multisig_Wallet(store) w.synchronize() @@ -99,6 +106,39 @@ def test_electrum_seed_old(self, mock_write): self.assertEqual(w.get_receiving_addresses()[0], '1FJEEB8ihPMbzs2SkLmr37dHyRFzakqUmo') self.assertEqual(w.get_change_addresses()[0], '1KRW8pH6HFHZh889VDq6fEKvmrsmApwNfe') + @mock.patch.object(storage.WalletStorage, '_write') + def test_electrum_seed_2fa(self, mock_write): + seed_words = 'kiss live scene rude gate step hip quarter bunker oxygen motor glove' + self.assertEqual(bitcoin.seed_type(seed_words), '2fa') + + xprv1, xpub1, xprv2, xpub2 = trustedcoin.TrustedCoinPlugin.xkeys_from_seed(seed_words, '') + + ks1 = keystore.from_xprv(xprv1) + self.assertTrue(isinstance(ks1, keystore.BIP32_KeyStore)) + self.assertEqual(ks1.xprv, 'xprv9uraXy9F3HP7i8QDqwNTBiD8Jf4bPD4Epif8cS8qbUbgeidUesyZpKmzfcSeHutsGfFnjgih7kzwTB5UQVRNB5LoXaNc8pFusKYx3KVVvYR') + self.assertEqual(ks1.xpub, 'xpub68qvwUg8sewQvcUgwxuTYr9rrgu5nfn6BwajQpYT9p8fXWxdCRHpN86UWruWJAD1ede8Sv8ERrTa22Gyc4SBfm7zFpcyoVWVBKCVwnw6s1J') + self.assertEqual(ks1.xpub, xpub1) + + ks2 = keystore.from_xprv(xprv2) + self.assertTrue(isinstance(ks2, keystore.BIP32_KeyStore)) + self.assertEqual(ks2.xprv, 'xprv9uraXy9F3HP7kKSiRAvLV7Nrjj7YzspDys7dvGLLu4tLZT49CEBxPWp88dHhVxvZ69SHrPQMUCWjj4Ka2z9kNvs1HAeEf3extGGeSWqEVqf') + self.assertEqual(ks2.xpub, 'xpub68qvwUg8sewQxoXBXCTLrFKbHkx3QLY5M63EiejxTQRKSFPHjmWCwK8byvZMM2wZNYA3SmxXoma3M1zxhGESHZwtB7SwrxRgKXAG8dCD2eS') + self.assertEqual(ks2.xpub, xpub2) + + long_user_id, short_id = trustedcoin.get_user_id( + {'x1/': {'xpub': xpub1}, + 'x2/': {'xpub': xpub2}}) + xpub3 = trustedcoin.make_xpub(trustedcoin.signing_xpub, long_user_id) + ks3 = keystore.from_xpub(xpub3) + self._check_xpub_keystore_sanity(ks3) + self.assertTrue(isinstance(ks3, keystore.BIP32_KeyStore)) + + w = self._create_multisig_wallet(ks1, ks2, ks3) + self.assertEqual(w.txin_type, 'p2sh') + + self.assertEqual(w.get_receiving_addresses()[0], '35L8XmCDoEBKeaWRjvmZvoZvhp8BXMMMPV') + self.assertEqual(w.get_change_addresses()[0], '3PeZEcumRqHSPNN43hd4yskGEBdzXgY8Cy') + @mock.patch.object(storage.WalletStorage, '_write') def test_bip39_seed_bip44_standard(self, mock_write): seed_words = 'treat dwarf wealth gasp brass outside high rent blood crowd make initial' diff --git a/plugins/trustedcoin/trustedcoin.py b/plugins/trustedcoin/trustedcoin.py index 20c6d1d3b5f3..a8f808224936 100644 --- a/plugins/trustedcoin/trustedcoin.py +++ b/plugins/trustedcoin/trustedcoin.py @@ -388,6 +388,7 @@ def create_seed(self, wizard): f = lambda x: wizard.request_passphrase(seed, x) wizard.show_seed_dialog(run_next=f, seed_text=seed) + @classmethod def get_xkeys(self, seed, passphrase, derivation): from electrum.mnemonic import Mnemonic from electrum.keystore import bip32_root, bip32_private_derivation @@ -396,6 +397,7 @@ def get_xkeys(self, seed, passphrase, derivation): xprv, xpub = bip32_private_derivation(xprv, "m/", derivation) return xprv, xpub + @classmethod def xkeys_from_seed(self, seed, passphrase): words = seed.split() n = len(words) From f9e9597381240adf1c67234d7a2defada07795a8 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 4 Jan 2018 23:41:10 +0100 Subject: [PATCH 08/11] make daemon lockfile not executable --- lib/daemon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/daemon.py b/lib/daemon.py index de2b13618088..58e1840efe95 100644 --- a/lib/daemon.py +++ b/lib/daemon.py @@ -58,7 +58,7 @@ def get_fd_or_server(config): lockfile = get_lockfile(config) while True: try: - return os.open(lockfile, os.O_CREAT | os.O_EXCL | os.O_WRONLY), None + return os.open(lockfile, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o644), None except OSError: pass server = get_server(config) From aeee5e907c5252169e621222beb7ace0f980bef7 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 5 Jan 2018 13:42:06 +0100 Subject: [PATCH 09/11] enable some translations in base_wizard.py --- lib/base_wizard.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/base_wizard.py b/lib/base_wizard.py index c379d45c8513..4eafd39ec974 100644 --- a/lib/base_wizard.py +++ b/lib/base_wizard.py @@ -294,7 +294,7 @@ def on_restore_seed(self, seed, is_bip39, is_ext): self.run('create_keystore', seed, '') elif self.seed_type == '2fa': if self.is_kivy: - self.show_error('2FA seeds are not supported in this version') + self.show_error(_('2FA seeds are not supported in this version')) self.run('restore_from_seed') else: self.load_2fa() @@ -386,10 +386,10 @@ def on_cosigner(self, text, password, i): def choose_seed_type(self): title = _('Choose Seed type') message = ' '.join([ - "The type of addresses used by your wallet will depend on your seed.", - "Segwit wallets use bech32 addresses, defined in BIP173.", - "Please note that websites and other wallets may not support these addresses yet.", - "Thus, you might want to keep using a non-segwit wallet in order to be able to receive bitcoins during the transition period." + _("The type of addresses used by your wallet will depend on your seed."), + _("Segwit wallets use bech32 addresses, defined in BIP173."), + _("Please note that websites and other wallets may not support these addresses yet."), + _("Thus, you might want to keep using a non-segwit wallet in order to be able to receive bitcoins during the transition period.") ]) choices = [ ('create_standard_seed', _('Standard')), From 0cf67997c01bef74233255a1d806d722e953eb36 Mon Sep 17 00:00:00 2001 From: Tristan Seligmann Date: Sat, 6 Jan 2018 08:42:54 +0200 Subject: [PATCH 10/11] Stop allowing CORS for the JSON-RPC server As far as I can tell, there is no need to allow this, and doing so poses severe security risks (see #3374). --- lib/daemon.py | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/lib/daemon.py b/lib/daemon.py index de2b13618088..d822ade91aa0 100644 --- a/lib/daemon.py +++ b/lib/daemon.py @@ -28,7 +28,7 @@ # from jsonrpc import JSONRPCResponseManager import jsonrpclib -from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer, SimpleJSONRPCRequestHandler +from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer from .version import ELECTRUM_VERSION from .network import Network @@ -87,19 +87,6 @@ def get_server(config): time.sleep(1.0) -class RequestHandler(SimpleJSONRPCRequestHandler): - - def do_OPTIONS(self): - self.send_response(200) - self.end_headers() - - def end_headers(self): - self.send_header("Access-Control-Allow-Headers", - "Origin, X-Requested-With, Content-Type, Accept") - self.send_header("Access-Control-Allow-Origin", "*") - SimpleJSONRPCRequestHandler.end_headers(self) - - class Daemon(DaemonThread): def __init__(self, config, fd): @@ -124,7 +111,7 @@ def init_server(self, config, fd): host = config.get('rpchost', '127.0.0.1') port = config.get('rpcport', 0) try: - server = SimpleJSONRPCServer((host, port), logRequests=False, requestHandler=RequestHandler) + server = SimpleJSONRPCServer((host, port), logRequests=False) except Exception as e: self.print_error('Warning: cannot initialize RPC server on host', host, e) self.server = None From f8fad2fd2970a171acb5cf89ceb590fb259db2ca Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 6 Jan 2018 22:45:41 +0100 Subject: [PATCH 11/11] release 3.0.4 --- RELEASE-NOTES | 10 ++++++++++ lib/version.py | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/RELEASE-NOTES b/RELEASE-NOTES index a0894e0a4aed..5f14a44651a0 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -1,3 +1,13 @@ +# Release 3.0.4 : (Security update) + + * Fix a vulnerability caused by Cross-Origin Resource Sharing (CORS) + in the JSONRPC interface. Previous versions of Electrum are + vulnerable to port scanning and deanonimization attacks from + malicious websites. Wallets that are not password-protected are + vulnerable to theft. + * Bundle QR scanner with Android app + * Minor bug fixes + # Release 3.0.3 * Qt GUI: sweeping now uses the Send tab, allowing fees to be set * Windows: if using the installer binary, there is now a separate shortcut diff --git a/lib/version.py b/lib/version.py index 304ebef518f4..687527d6993d 100644 --- a/lib/version.py +++ b/lib/version.py @@ -1,4 +1,4 @@ -ELECTRUM_VERSION = '3.0.3' # version of the client package +ELECTRUM_VERSION = '3.0.4' # version of the client package PROTOCOL_VERSION = '1.1' # protocol version requested # The hash of the mnemonic seed must begin with this