Compare commits

...

6 Commits

Author SHA1 Message Date
ThomasV
c26cc844f5 version 2.9.4 2018-01-12 17:03:23 +01:00
Neil Booth
2be9d0a25a Remove decrypt_message; no longer supported by Trezor or KeepKey 2018-01-12 16:54:56 +01:00
SomberNight
d0f9e20cf7 storage: refuse to open newer version wallet files
This is mostly a manual copy of parts of multiple commits from the master branch.
2018-01-12 16:52:36 +01:00
SomberNight
64d998fae8 follow-up 387f6db3b6 - py2 fixes 2018-01-12 16:25:23 +01:00
SomberNight
387f6db3b6 Password-protect the JSON RPC interface 2018-01-12 15:22:38 +01:00
ThomasV
af0715e476 backport security updates: disable CORS and JSONRPC in gui 2018-01-12 15:10:59 +01:00
10 changed files with 211 additions and 49 deletions

View File

@ -1,3 +1,7 @@
# Release 2.9.4 (security update)
* Backport security fixes from 3.0.5 after vulnerability was
discovered in JSONRPC interface.
# Release 2.9.3
* fix configuration file issue #2719
* fix ledger signing of non-RBF transactions

View File

@ -372,7 +372,7 @@ if __name__ == '__main__':
fd, server = daemon.get_fd_or_server(config)
if fd is not None:
plugins = init_plugins(config, config.get('gui', 'qt'))
d = daemon.Daemon(config, fd)
d = daemon.Daemon(config, fd, True)
d.start()
d.init_gui(config, plugins)
sys.exit(0)
@ -393,7 +393,7 @@ if __name__ == '__main__':
print_stderr("starting daemon (PID %d)" % pid)
sys.exit(0)
init_plugins(config, 'cmdline')
d = daemon.Daemon(config, fd)
d = daemon.Daemon(config, fd, False)
d.start()
if config.get('websocket_server'):
from electrum import websockets

View File

@ -168,7 +168,12 @@ class ElectrumGui:
w.bring_to_top()
break
else:
wallet = self.daemon.load_wallet(path, None)
try:
wallet = self.daemon.load_wallet(path, None)
except BaseException as e:
d = QMessageBox(QMessageBox.Warning, _('Error'), 'Cannot load wallet:\n' + str(e))
d.exec_()
return
if not wallet:
storage = WalletStorage(path)
wizard = InstallWizard(self.config, self.app, self.plugins, storage)

View File

@ -29,12 +29,12 @@ import sys
import time
import jsonrpclib
from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer, SimpleJSONRPCRequestHandler
from .jsonrpc import VerifyingJSONRPCServer
from version import ELECTRUM_VERSION
from network import Network
from util import json_decode, DaemonThread
from util import print_msg, print_error, print_stderr, UserCancelled
from util import (json_decode, DaemonThread, print_msg, print_error,
print_stderr, UserCancelled, to_string, int_to_bytes)
from wallet import Wallet
from storage import WalletStorage
from commands import known_commands, Commands
@ -73,7 +73,14 @@ def get_server(config):
try:
with open(lockfile) as f:
(host, port), create_time = ast.literal_eval(f.read())
server = jsonrpclib.Server('http://%s:%d' % (host, port))
rpc_user, rpc_password = get_rpc_credentials(config)
if rpc_password == '':
# authentication disabled
server_url = 'http://%s:%d' % (host, port)
else:
server_url = 'http://%s:%s@%s:%d' % (
rpc_user, rpc_password, host, port)
server = jsonrpclib.Server(server_url)
# Test daemon is running
server.ping()
return server
@ -85,23 +92,29 @@ 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)
def get_rpc_credentials(config):
rpc_user = config.get('rpcuser', None)
rpc_password = config.get('rpcpassword', None)
if rpc_user is None or rpc_password is None:
rpc_user = 'user'
import ecdsa, base64
bits = 128
nbytes = bits // 8 + (bits % 8 > 0)
pw_int = ecdsa.util.randrange(pow(2, bits))
pw_b64 = base64.b64encode(
int_to_bytes(pw_int, nbytes, 'big'), b'-_')
rpc_password = to_string(pw_b64, 'ascii')
config.set_key('rpcuser', rpc_user)
config.set_key('rpcpassword', rpc_password, save=True)
elif rpc_password == '':
from .util import print_stderr
print_stderr('WARNING: RPC authentication is disabled.')
return rpc_user, rpc_password
class Daemon(DaemonThread):
def __init__(self, config, fd):
def __init__(self, config, fd, is_gui):
DaemonThread.__init__(self)
self.config = config
if config.get('offline'):
@ -116,30 +129,34 @@ class Daemon(DaemonThread):
self.gui = None
self.wallets = {}
# Setup JSONRPC server
self.cmd_runner = Commands(self.config, None, self.network)
self.init_server(config, fd)
self.init_server(config, fd, is_gui)
def init_server(self, config, fd):
def init_server(self, config, fd, is_gui):
host = config.get('rpchost', '127.0.0.1')
port = config.get('rpcport', 0)
rpc_user, rpc_password = get_rpc_credentials(config)
try:
server = SimpleJSONRPCServer((host, port), logRequests=False,
requestHandler=RequestHandler)
except:
self.print_error('Warning: cannot initialize RPC server on host', host)
server = VerifyingJSONRPCServer((host, port), logRequests=False,
rpc_user=rpc_user, rpc_password=rpc_password)
except Exception as e:
self.print_error('Warning: cannot initialize RPC server on host', host, e)
self.server = None
os.close(fd)
return
os.write(fd, repr((server.socket.getsockname(), time.time())))
os.close(fd)
server.timeout = 0.1
for cmdname in known_commands:
server.register_function(getattr(self.cmd_runner, cmdname), cmdname)
server.register_function(self.run_cmdline, 'run_cmdline')
server.register_function(self.ping, 'ping')
server.register_function(self.run_daemon, 'daemon')
server.register_function(self.run_gui, 'gui')
self.server = server
server.timeout = 0.1
server.register_function(self.ping, 'ping')
if is_gui:
server.register_function(self.run_gui, 'gui')
else:
self.cmd_runner = Commands(self.config, None, self.network)
for cmdname in known_commands:
server.register_function(getattr(self.cmd_runner, cmdname), cmdname)
server.register_function(self.run_cmdline, 'run_cmdline')
server.register_function(self.run_daemon, 'daemon')
def ping(self):
return True

97
lib/jsonrpc.py Normal file
View File

@ -0,0 +1,97 @@
#!/usr/bin/env python2
#
# Electrum - lightweight Bitcoin client
# Copyright (C) 2018 Thomas Voegtlin
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer, SimpleJSONRPCRequestHandler
from base64 import b64decode
import time
from . import util
class RPCAuthCredentialsInvalid(Exception):
def __str__(self):
return 'Authentication failed (bad credentials)'
class RPCAuthCredentialsMissing(Exception):
def __str__(self):
return 'Authentication failed (missing credentials)'
class RPCAuthUnsupportedType(Exception):
def __str__(self):
return 'Authentication failed (only basic auth is supported)'
# based on http://acooke.org/cute/BasicHTTPA0.html by andrew cooke
class VerifyingJSONRPCServer(SimpleJSONRPCServer):
def __init__(self, *args, **kargs):
self.rpc_user = kargs['rpc_user']
self.rpc_password = kargs['rpc_password']
del kargs['rpc_user']
del kargs['rpc_password']
class VerifyingRequestHandler(SimpleJSONRPCRequestHandler):
def parse_request(myself):
# first, call the original implementation which returns
# True if all OK so far
if SimpleJSONRPCRequestHandler.parse_request(myself):
try:
self.authenticate(myself.headers)
return True
except (RPCAuthCredentialsInvalid, RPCAuthCredentialsMissing,
RPCAuthUnsupportedType) as e:
myself.send_error(401, str(e))
except BaseException as e:
import traceback, sys
traceback.print_exc(file=sys.stderr)
myself.send_error(500, str(e))
return False
SimpleJSONRPCServer.__init__(
self, requestHandler=VerifyingRequestHandler, *args, **kargs)
def authenticate(self, headers):
if self.rpc_password == '':
# RPC authentication is disabled
return
auth_string = headers.get('Authorization', None)
if auth_string is None:
raise RPCAuthCredentialsMissing()
(basic, _, encoded) = auth_string.partition(' ')
if basic != 'Basic':
raise RPCAuthUnsupportedType()
encoded = util.to_bytes(encoded, 'utf8')
credentials = util.to_string(b64decode(encoded), 'utf8')
(username, _, password) = credentials.partition(':')
if not (util.constant_time_compare(username, self.rpc_user)
and util.constant_time_compare(password, self.rpc_password)):
time.sleep(0.050)
raise RPCAuthCredentialsInvalid()

View File

@ -75,6 +75,9 @@ class WalletStorage(PrintError):
self.raw = f.read()
if not self.is_encrypted():
self.load_data(self.raw)
else:
# avoid new wallets getting 'upgraded'
self.put('seed_version', FINAL_SEED_VERSION)
def load_data(self, s):
try:
@ -161,8 +164,6 @@ class WalletStorage(PrintError):
@profiler
def write(self):
# this ensures that previous versions of electrum won't open the wallet
self.put('seed_version', FINAL_SEED_VERSION)
with self.lock:
self._write()
@ -244,12 +245,14 @@ class WalletStorage(PrintError):
return result
def requires_upgrade(self):
return self.file_exists() and self.get_seed_version() != FINAL_SEED_VERSION
return self.file_exists() and self.get_seed_version() < FINAL_SEED_VERSION
def upgrade(self):
self.convert_imported()
self.convert_wallet_type()
self.convert_account()
self.put('seed_version', FINAL_SEED_VERSION)
self.write()
def convert_wallet_type(self):
@ -379,6 +382,8 @@ class WalletStorage(PrintError):
seed_version = self.get('seed_version')
if not seed_version:
seed_version = OLD_SEED_VERSION if len(self.get('master_public_key','')) == 128 else NEW_SEED_VERSION
if seed_version > FINAL_SEED_VERSION:
raise BaseException('This version of Electrum is too old to open this wallet')
if seed_version >=12:
return seed_version
if seed_version not in [OLD_SEED_VERSION, NEW_SEED_VERSION]:

View File

@ -34,6 +34,8 @@ import urlparse
import urllib
import threading
from i18n import _
import hmac
base_units = {'BTC':8, 'mBTC':5, 'uBTC':2}
fee_levels = [_('Within 25 blocks'), _('Within 10 blocks'), _('Within 5 blocks'), _('Within 2 blocks'), _('In the next block')]
@ -191,6 +193,13 @@ def json_decode(x):
except:
return x
# taken from Django Source Code
def constant_time_compare(val1, val2):
"""Return True if the two strings are equal, False otherwise."""
return hmac.compare_digest(to_bytes(val1, 'utf8'), to_bytes(val2, 'utf8'))
# decorator that prints execution time
def profiler(func):
def do_profile(func, args, kw_args):
@ -238,6 +247,38 @@ def android_check_data_dir():
def get_headers_dir(config):
return android_headers_dir() if 'ANDROID_DATA' in os.environ else config.path
def to_string(x, enc):
if isinstance(x, (bytes, bytearray)):
return x.decode(enc)
if isinstance(x, str):
return x
else:
raise TypeError("Not a string or bytes like object")
def to_bytes(something, encoding='utf8'):
"""
cast string to bytes() like object, but for python2 support it's bytearray copy
"""
if isinstance(something, bytes):
return something
if isinstance(something, str) or isinstance(something, unicode):
return something.encode(encoding)
elif isinstance(something, bytearray):
return bytes(something)
else:
raise TypeError("Not a string or bytes like object")
# based on https://stackoverflow.com/questions/16022556/has-python-3-to-bytes-been-back-ported-to-python-2-7
def int_to_bytes(n, length, endianess='big'):
hex_n = '%x' % n
hex_n2 = '0'*(len(hex_n) % 2) + hex_n
left_padded_hex_n = hex_n2.zfill(length*2)
if len(left_padded_hex_n) > length*2:
raise OverflowError()
assert len(left_padded_hex_n) == length*2
bytes_n = left_padded_hex_n.decode('hex')
return bytes_n if endianess == 'big' else bytes_n[::-1]
def user_dir():
if 'ANDROID_DATA' in os.environ:
return android_check_data_dir()

View File

@ -1,4 +1,4 @@
ELECTRUM_VERSION = '2.9.3' # version of the client package
ELECTRUM_VERSION = '2.9.4' # version of the client package
PROTOCOL_VERSION = '0.10' # protocol version requested
# The hash of the mnemonic seed must begin with this

View File

@ -229,7 +229,7 @@ class TrezorClientBase(GuiMixin, PrintError):
@staticmethod
def wrap_methods(cls):
for method in ['apply_settings', 'change_pin', 'decrypt_message',
for method in ['apply_settings', 'change_pin',
'get_address', 'get_public_node',
'load_device_by_mnemonic', 'load_device_by_xprv',
'recovery_device', 'reset_device', 'sign_message',

View File

@ -29,14 +29,7 @@ class TrezorCompatibleKeyStore(Hardware_KeyStore):
return self.plugin.get_client(self, force_pair)
def decrypt_message(self, sequence, message, password):
raise RuntimeError(_('Electrum and %s encryption and decryption are currently incompatible') % self.device)
client = self.get_client()
address_path = self.get_derivation() + "/%d/%d"%sequence
address_n = client.expand_path(address_path)
payload = base64.b64decode(message)
nonce, message, msg_hmac = payload[:33], payload[33:-8], payload[-8:]
result = client.decrypt_message(address_n, nonce, message, msg_hmac)
return result.message
raise RuntimeError(_('Encryption and decryption are not implemented by %s') % self.device)
def sign_message(self, sequence, message, password):
client = self.get_client()
@ -55,7 +48,7 @@ class TrezorCompatibleKeyStore(Hardware_KeyStore):
for txin in tx.inputs():
pubkeys, x_pubkeys = tx.get_sorted_pubkeys(txin)
tx_hash = txin['prevout_hash']
prev_tx[tx_hash] = txin['prev_tx']
prev_tx[tx_hash] = txin['prev_tx']
for x_pubkey in x_pubkeys:
if not is_xpubkey(x_pubkey):
continue
@ -96,7 +89,7 @@ class TrezorCompatiblePlugin(HW_PluginBase):
# raise
self.print_error("cannot connect at", device.path, str(e))
return None
def _try_bridge(self, device):
self.print_error("Trying to connect over Trezor Bridge...")
try: