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 # Release 2.9.3
* fix configuration file issue #2719 * fix configuration file issue #2719
* fix ledger signing of non-RBF transactions * fix ledger signing of non-RBF transactions

View File

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

View File

@ -168,7 +168,12 @@ class ElectrumGui:
w.bring_to_top() w.bring_to_top()
break break
else: 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: if not wallet:
storage = WalletStorage(path) storage = WalletStorage(path)
wizard = InstallWizard(self.config, self.app, self.plugins, storage) wizard = InstallWizard(self.config, self.app, self.plugins, storage)

View File

@ -29,12 +29,12 @@ import sys
import time import time
import jsonrpclib import jsonrpclib
from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer, SimpleJSONRPCRequestHandler from .jsonrpc import VerifyingJSONRPCServer
from version import ELECTRUM_VERSION from version import ELECTRUM_VERSION
from network import Network from network import Network
from util import json_decode, DaemonThread from util import (json_decode, DaemonThread, print_msg, print_error,
from util import print_msg, print_error, print_stderr, UserCancelled print_stderr, UserCancelled, to_string, int_to_bytes)
from wallet import Wallet from wallet import Wallet
from storage import WalletStorage from storage import WalletStorage
from commands import known_commands, Commands from commands import known_commands, Commands
@ -73,7 +73,14 @@ def get_server(config):
try: try:
with open(lockfile) as f: with open(lockfile) as f:
(host, port), create_time = ast.literal_eval(f.read()) (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 # Test daemon is running
server.ping() server.ping()
return server return server
@ -85,23 +92,29 @@ def get_server(config):
time.sleep(1.0) time.sleep(1.0)
def get_rpc_credentials(config):
class RequestHandler(SimpleJSONRPCRequestHandler): rpc_user = config.get('rpcuser', None)
rpc_password = config.get('rpcpassword', None)
def do_OPTIONS(self): if rpc_user is None or rpc_password is None:
self.send_response(200) rpc_user = 'user'
self.end_headers() import ecdsa, base64
bits = 128
def end_headers(self): nbytes = bits // 8 + (bits % 8 > 0)
self.send_header("Access-Control-Allow-Headers", pw_int = ecdsa.util.randrange(pow(2, bits))
"Origin, X-Requested-With, Content-Type, Accept") pw_b64 = base64.b64encode(
self.send_header("Access-Control-Allow-Origin", "*") int_to_bytes(pw_int, nbytes, 'big'), b'-_')
SimpleJSONRPCRequestHandler.end_headers(self) 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): class Daemon(DaemonThread):
def __init__(self, config, fd): def __init__(self, config, fd, is_gui):
DaemonThread.__init__(self) DaemonThread.__init__(self)
self.config = config self.config = config
if config.get('offline'): if config.get('offline'):
@ -116,30 +129,34 @@ class Daemon(DaemonThread):
self.gui = None self.gui = None
self.wallets = {} self.wallets = {}
# Setup JSONRPC server # Setup JSONRPC server
self.cmd_runner = Commands(self.config, None, self.network) self.init_server(config, fd, is_gui)
self.init_server(config, fd)
def init_server(self, config, fd): def init_server(self, config, fd, is_gui):
host = config.get('rpchost', '127.0.0.1') host = config.get('rpchost', '127.0.0.1')
port = config.get('rpcport', 0) port = config.get('rpcport', 0)
rpc_user, rpc_password = get_rpc_credentials(config)
try: try:
server = SimpleJSONRPCServer((host, port), logRequests=False, server = VerifyingJSONRPCServer((host, port), logRequests=False,
requestHandler=RequestHandler) rpc_user=rpc_user, rpc_password=rpc_password)
except: except Exception as e:
self.print_error('Warning: cannot initialize RPC server on host', host) self.print_error('Warning: cannot initialize RPC server on host', host, e)
self.server = None self.server = None
os.close(fd) os.close(fd)
return return
os.write(fd, repr((server.socket.getsockname(), time.time()))) os.write(fd, repr((server.socket.getsockname(), time.time())))
os.close(fd) 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 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): def ping(self):
return True 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() self.raw = f.read()
if not self.is_encrypted(): if not self.is_encrypted():
self.load_data(self.raw) self.load_data(self.raw)
else:
# avoid new wallets getting 'upgraded'
self.put('seed_version', FINAL_SEED_VERSION)
def load_data(self, s): def load_data(self, s):
try: try:
@ -161,8 +164,6 @@ class WalletStorage(PrintError):
@profiler @profiler
def write(self): 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: with self.lock:
self._write() self._write()
@ -244,12 +245,14 @@ class WalletStorage(PrintError):
return result return result
def requires_upgrade(self): 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): def upgrade(self):
self.convert_imported() self.convert_imported()
self.convert_wallet_type() self.convert_wallet_type()
self.convert_account() self.convert_account()
self.put('seed_version', FINAL_SEED_VERSION)
self.write() self.write()
def convert_wallet_type(self): def convert_wallet_type(self):
@ -379,6 +382,8 @@ class WalletStorage(PrintError):
seed_version = self.get('seed_version') seed_version = self.get('seed_version')
if not seed_version: if not seed_version:
seed_version = OLD_SEED_VERSION if len(self.get('master_public_key','')) == 128 else NEW_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: if seed_version >=12:
return seed_version return seed_version
if seed_version not in [OLD_SEED_VERSION, NEW_SEED_VERSION]: if seed_version not in [OLD_SEED_VERSION, NEW_SEED_VERSION]:

View File

@ -34,6 +34,8 @@ import urlparse
import urllib import urllib
import threading import threading
from i18n import _ from i18n import _
import hmac
base_units = {'BTC':8, 'mBTC':5, 'uBTC':2} 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')] 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: except:
return x 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 # decorator that prints execution time
def profiler(func): def profiler(func):
def do_profile(func, args, kw_args): def do_profile(func, args, kw_args):
@ -238,6 +247,38 @@ def android_check_data_dir():
def get_headers_dir(config): def get_headers_dir(config):
return android_headers_dir() if 'ANDROID_DATA' in os.environ else config.path 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(): def user_dir():
if 'ANDROID_DATA' in os.environ: if 'ANDROID_DATA' in os.environ:
return android_check_data_dir() 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 PROTOCOL_VERSION = '0.10' # protocol version requested
# The hash of the mnemonic seed must begin with this # The hash of the mnemonic seed must begin with this

View File

@ -229,7 +229,7 @@ class TrezorClientBase(GuiMixin, PrintError):
@staticmethod @staticmethod
def wrap_methods(cls): 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', 'get_address', 'get_public_node',
'load_device_by_mnemonic', 'load_device_by_xprv', 'load_device_by_mnemonic', 'load_device_by_xprv',
'recovery_device', 'reset_device', 'sign_message', 'recovery_device', 'reset_device', 'sign_message',

View File

@ -29,14 +29,7 @@ class TrezorCompatibleKeyStore(Hardware_KeyStore):
return self.plugin.get_client(self, force_pair) return self.plugin.get_client(self, force_pair)
def decrypt_message(self, sequence, message, password): def decrypt_message(self, sequence, message, password):
raise RuntimeError(_('Electrum and %s encryption and decryption are currently incompatible') % self.device) raise RuntimeError(_('Encryption and decryption are not implemented by %s') % 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
def sign_message(self, sequence, message, password): def sign_message(self, sequence, message, password):
client = self.get_client() client = self.get_client()