diff --git a/RELEASE-NOTES b/RELEASE-NOTES index 8520a4bb34f7..57f9e962aceb 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/gui/qt/main_window.py b/gui/qt/main_window.py index 27ba897b44e5..124e652667f6 100644 --- a/gui/qt/main_window.py +++ b/gui/qt/main_window.py @@ -2016,24 +2016,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) @@ -2045,7 +2047,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 diff --git a/lib/base_wizard.py b/lib/base_wizard.py index 44461110d5e5..e08ff1f403f9 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')), diff --git a/lib/daemon.py b/lib/daemon.py index 80e970e7faa0..2d9c9450b397 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 @@ -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) @@ -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 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 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): diff --git a/lib/tests/test_wallet_vertical.py b/lib/tests/test_wallet_vertical.py index e6deb448a4f0..1b4f1e3e0740 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], 'MNCPTc38CQXQtXzmyKRnHPEdSe9A6RWaht') self.assertEqual(w.get_change_addresses()[0], 'MSKfNFBVnGTNao6UiCV2uVwGF5kvm2QBtq') + #@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/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 diff --git a/lib/wallet.py b/lib/wallet.py index 48314628a689..fc8a6524abc0 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -984,7 +984,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) diff --git a/plugins/ledger/ledger.py b/plugins/ledger/ledger.py index 5e116dca29cd..c8c480c5be8b 100644 --- a/plugins/ledger/ledger.py +++ b/plugins/ledger/ledger.py @@ -221,10 +221,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().upper() # 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 = ""