Wallet file encryption:
- a keypair is derived from the wallet password - only the public key is retained in memory - wallets must opened and closed explicitly with the daemon
This commit is contained in:
parent
7e76e4ac55
commit
fcc92c1ebd
38
electrum
38
electrum
|
@ -179,7 +179,27 @@ def run_non_RPC(config):
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
def init_cmdline(config_options):
|
def init_daemon(config_options):
|
||||||
|
config = SimpleConfig(config_options)
|
||||||
|
storage = WalletStorage(config.get_wallet_path())
|
||||||
|
if not storage.file_exists:
|
||||||
|
print_msg("Error: Wallet file not found.")
|
||||||
|
print_msg("Type 'electrum create' to create a new wallet, or provide a path to a wallet with the -w option")
|
||||||
|
sys.exit(0)
|
||||||
|
if storage.is_encrypted():
|
||||||
|
if config.get('password'):
|
||||||
|
password = config.get('password')
|
||||||
|
else:
|
||||||
|
password = prompt_password('Password:', False)
|
||||||
|
if not password:
|
||||||
|
print_msg("Error: Password required")
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
password = None
|
||||||
|
config_options['password'] = password
|
||||||
|
|
||||||
|
|
||||||
|
def init_cmdline(config_options, server):
|
||||||
config = SimpleConfig(config_options)
|
config = SimpleConfig(config_options)
|
||||||
cmdname = config.get('cmd')
|
cmdname = config.get('cmd')
|
||||||
cmd = known_commands[cmdname]
|
cmd = known_commands[cmdname]
|
||||||
|
@ -208,8 +228,11 @@ def init_cmdline(config_options):
|
||||||
print_stderr("Exposing a single private key can compromise your entire wallet!")
|
print_stderr("Exposing a single private key can compromise your entire wallet!")
|
||||||
print_stderr("In particular, DO NOT use 'redeem private key' services proposed by third parties.")
|
print_stderr("In particular, DO NOT use 'redeem private key' services proposed by third parties.")
|
||||||
|
|
||||||
|
if not storage.is_encrypted():
|
||||||
|
storage.read(None)
|
||||||
# commands needing password
|
# commands needing password
|
||||||
if cmd.requires_password and storage.get('use_encryption'):
|
if (storage.is_encrypted() and server is None)\
|
||||||
|
or (cmd.requires_password and (storage.get('use_encryption') or storage.is_encrypted())):
|
||||||
if config.get('password'):
|
if config.get('password'):
|
||||||
password = config.get('password')
|
password = config.get('password')
|
||||||
else:
|
else:
|
||||||
|
@ -232,18 +255,19 @@ def init_cmdline(config_options):
|
||||||
def run_offline_command(config, config_options):
|
def run_offline_command(config, config_options):
|
||||||
cmdname = config.get('cmd')
|
cmdname = config.get('cmd')
|
||||||
cmd = known_commands[cmdname]
|
cmd = known_commands[cmdname]
|
||||||
|
password = config_options.get('password')
|
||||||
storage = WalletStorage(config.get_wallet_path())
|
storage = WalletStorage(config.get_wallet_path())
|
||||||
|
storage.read(password if storage.is_encrypted() else None)
|
||||||
wallet = Wallet(storage) if cmd.requires_wallet else None
|
wallet = Wallet(storage) if cmd.requires_wallet else None
|
||||||
# check password
|
# check password
|
||||||
if cmd.requires_password and storage.get('use_encryption'):
|
if cmd.requires_password and storage.get('use_encryption'):
|
||||||
password = config_options.get('password')
|
|
||||||
try:
|
try:
|
||||||
seed = wallet.check_password(password)
|
seed = wallet.check_password(password)
|
||||||
except InvalidPassword:
|
except InvalidPassword:
|
||||||
print_msg("Error: This password does not decode this wallet.")
|
print_msg("Error: This password does not decode this wallet.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if cmd.requires_network:
|
if cmd.requires_network:
|
||||||
print_stderr("Warning: running command offline")
|
print_msg("Warning: running command offline")
|
||||||
# arguments passed to function
|
# arguments passed to function
|
||||||
args = map(lambda x: config.get(x), cmd.params)
|
args = map(lambda x: config.get(x), cmd.params)
|
||||||
# decode json arguments
|
# decode json arguments
|
||||||
|
@ -347,7 +371,9 @@ if __name__ == '__main__':
|
||||||
|
|
||||||
elif cmdname == 'daemon':
|
elif cmdname == 'daemon':
|
||||||
subcommand = config.get('subcommand')
|
subcommand = config.get('subcommand')
|
||||||
assert subcommand in [None, 'start', 'stop', 'status']
|
if subcommand in ['open']:
|
||||||
|
init_daemon(config_options)
|
||||||
|
|
||||||
if subcommand in [None, 'start']:
|
if subcommand in [None, 'start']:
|
||||||
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:
|
||||||
|
@ -377,8 +403,8 @@ if __name__ == '__main__':
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
else:
|
else:
|
||||||
# command line
|
# command line
|
||||||
init_cmdline(config_options)
|
|
||||||
server = daemon.get_server(config)
|
server = daemon.get_server(config)
|
||||||
|
init_cmdline(config_options, server)
|
||||||
if server is not None:
|
if server is not None:
|
||||||
result = server.run_cmdline(config_options)
|
result = server.run_cmdline(config_options)
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -159,18 +159,26 @@ class ElectrumGui:
|
||||||
w.bring_to_top()
|
w.bring_to_top()
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
try:
|
if not os.path.exists(path):
|
||||||
wallet = self.daemon.load_wallet(path)
|
|
||||||
except BaseException as e:
|
|
||||||
QMessageBox.information(None, _('Error'), str(e), _('OK'))
|
|
||||||
return
|
|
||||||
if wallet is None:
|
|
||||||
wizard = InstallWizard(self.config, self.app, self.plugins, path)
|
wizard = InstallWizard(self.config, self.app, self.plugins, path)
|
||||||
wallet = wizard.run_and_get_wallet()
|
wallet = wizard.run_and_get_wallet()
|
||||||
if not wallet:
|
if not wallet:
|
||||||
return
|
return
|
||||||
wallet.start_threads(self.daemon.network)
|
wallet.start_threads(self.daemon.network)
|
||||||
self.daemon.add_wallet(wallet)
|
self.daemon.add_wallet(wallet)
|
||||||
|
else:
|
||||||
|
from password_dialog import PasswordDialog
|
||||||
|
msg = _("The file '%s' is encrypted.") % os.path.basename(path)
|
||||||
|
password_getter = lambda: PasswordDialog(msg=msg).run()
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
wallet = self.daemon.load_wallet(path, password_getter)
|
||||||
|
break
|
||||||
|
except UserCancelled:
|
||||||
|
return
|
||||||
|
except BaseException as e:
|
||||||
|
QMessageBox.information(None, _('Error'), str(e), _('OK'))
|
||||||
|
continue
|
||||||
w = self.create_window_for_wallet(wallet)
|
w = self.create_window_for_wallet(wallet)
|
||||||
if uri:
|
if uri:
|
||||||
w.pay_to_URI(uri)
|
w.pay_to_URI(uri)
|
||||||
|
|
|
@ -294,8 +294,9 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
|
||||||
|
|
||||||
def pw_layout(self, msg, kind):
|
def pw_layout(self, msg, kind):
|
||||||
playout = PasswordLayout(None, msg, kind, self.next_button)
|
playout = PasswordLayout(None, msg, kind, self.next_button)
|
||||||
|
playout.encrypt_cb.setChecked(True)
|
||||||
self.set_main_layout(playout.layout())
|
self.set_main_layout(playout.layout())
|
||||||
return playout.new_password()
|
return playout.new_password(), playout.encrypt_cb.isChecked()
|
||||||
|
|
||||||
@wizard_dialog
|
@wizard_dialog
|
||||||
def request_password(self, run_next):
|
def request_password(self, run_next):
|
||||||
|
|
|
@ -1680,19 +1680,13 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
self.send_button.setVisible(not self.wallet.is_watching_only())
|
self.send_button.setVisible(not self.wallet.is_watching_only())
|
||||||
|
|
||||||
def change_password_dialog(self):
|
def change_password_dialog(self):
|
||||||
from password_dialog import PasswordDialog, PW_CHANGE
|
from password_dialog import ChangePasswordDialog
|
||||||
|
d = ChangePasswordDialog(self, self.wallet)
|
||||||
msg = (_('Your wallet is encrypted. Use this dialog to change your '
|
ok, password, new_password, encrypt_file = d.run()
|
||||||
'password. To disable wallet encryption, enter an empty new '
|
|
||||||
'password.') if self.wallet.has_password()
|
|
||||||
else _('Your wallet keys are not encrypted'))
|
|
||||||
d = PasswordDialog(self, self.wallet, msg, PW_CHANGE)
|
|
||||||
ok, password, new_password = d.run()
|
|
||||||
if not ok:
|
if not ok:
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.wallet.update_password(password, new_password)
|
self.wallet.update_password(password, new_password, encrypt_file)
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
self.show_error(str(e))
|
self.show_error(str(e))
|
||||||
return
|
return
|
||||||
|
@ -1700,8 +1694,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
traceback.print_exc(file=sys.stdout)
|
traceback.print_exc(file=sys.stdout)
|
||||||
self.show_error(_('Failed to update password'))
|
self.show_error(_('Failed to update password'))
|
||||||
return
|
return
|
||||||
|
msg = _('Password was updated successfully') if new_password else _('Password is disabled, this wallet is not protected')
|
||||||
msg = _('Password was updated successfully') if new_password else _('This wallet is not encrypted')
|
|
||||||
self.show_message(msg, title=_("Success"))
|
self.show_message(msg, title=_("Success"))
|
||||||
self.update_lock_icon()
|
self.update_lock_icon()
|
||||||
|
|
||||||
|
@ -1972,24 +1965,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
d.exec_()
|
d.exec_()
|
||||||
|
|
||||||
def password_dialog(self, msg=None, parent=None):
|
def password_dialog(self, msg=None, parent=None):
|
||||||
|
from password_dialog import PasswordDialog
|
||||||
parent = parent or self
|
parent = parent or self
|
||||||
d = WindowModalDialog(parent, _("Enter Password"))
|
d = PasswordDialog(parent, msg)
|
||||||
pw = QLineEdit()
|
return d.run()
|
||||||
pw.setEchoMode(2)
|
|
||||||
vbox = QVBoxLayout()
|
|
||||||
if not msg:
|
|
||||||
msg = _('Please enter your password')
|
|
||||||
vbox.addWidget(QLabel(msg))
|
|
||||||
grid = QGridLayout()
|
|
||||||
grid.setSpacing(8)
|
|
||||||
grid.addWidget(QLabel(_('Password')), 1, 0)
|
|
||||||
grid.addWidget(pw, 1, 1)
|
|
||||||
vbox.addLayout(grid)
|
|
||||||
vbox.addLayout(Buttons(CancelButton(d), OkButton(d)))
|
|
||||||
d.setLayout(vbox)
|
|
||||||
run_hook('password_dialog', pw, grid, 1)
|
|
||||||
if not d.exec_(): return
|
|
||||||
return unicode(pw.text())
|
|
||||||
|
|
||||||
|
|
||||||
def tx_from_text(self, txt):
|
def tx_from_text(self, txt):
|
||||||
|
|
|
@ -30,6 +30,8 @@ from util import *
|
||||||
import re
|
import re
|
||||||
import math
|
import math
|
||||||
|
|
||||||
|
from electrum.plugins import run_hook
|
||||||
|
|
||||||
def check_password_strength(password):
|
def check_password_strength(password):
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
@ -92,7 +94,7 @@ class PasswordLayout(object):
|
||||||
logo_grid.addWidget(label, 0, 1, 1, 2)
|
logo_grid.addWidget(label, 0, 1, 1, 2)
|
||||||
vbox.addLayout(logo_grid)
|
vbox.addLayout(logo_grid)
|
||||||
|
|
||||||
m1 = _('New Password:') if kind == PW_NEW else _('Password:')
|
m1 = _('New Password:') if kind == PW_CHANGE else _('Password:')
|
||||||
msgs = [m1, _('Confirm Password:')]
|
msgs = [m1, _('Confirm Password:')]
|
||||||
if wallet and wallet.has_password():
|
if wallet and wallet.has_password():
|
||||||
grid.addWidget(QLabel(_('Current Password:')), 0, 0)
|
grid.addWidget(QLabel(_('Current Password:')), 0, 0)
|
||||||
|
@ -115,8 +117,15 @@ class PasswordLayout(object):
|
||||||
grid.addWidget(self.pw_strength, 3, 0, 1, 2)
|
grid.addWidget(self.pw_strength, 3, 0, 1, 2)
|
||||||
self.new_pw.textChanged.connect(self.pw_changed)
|
self.new_pw.textChanged.connect(self.pw_changed)
|
||||||
|
|
||||||
|
self.encrypt_cb = QCheckBox(_('Encrypt wallet file'))
|
||||||
|
self.encrypt_cb.setEnabled(False)
|
||||||
|
grid.addWidget(self.encrypt_cb, 4, 0, 1, 2)
|
||||||
|
self.encrypt_cb.setVisible(kind != PW_PASSPHRASE)
|
||||||
|
|
||||||
def enable_OK():
|
def enable_OK():
|
||||||
OK_button.setEnabled(self.new_pw.text() == self.conf_pw.text())
|
ok = self.new_pw.text() == self.conf_pw.text()
|
||||||
|
OK_button.setEnabled(ok)
|
||||||
|
self.encrypt_cb.setEnabled(ok and bool(self.new_pw.text()))
|
||||||
self.new_pw.textChanged.connect(enable_OK)
|
self.new_pw.textChanged.connect(enable_OK)
|
||||||
self.conf_pw.textChanged.connect(enable_OK)
|
self.conf_pw.textChanged.connect(enable_OK)
|
||||||
|
|
||||||
|
@ -153,20 +162,54 @@ class PasswordLayout(object):
|
||||||
return pw
|
return pw
|
||||||
|
|
||||||
|
|
||||||
class PasswordDialog(WindowModalDialog):
|
class ChangePasswordDialog(WindowModalDialog):
|
||||||
|
|
||||||
def __init__(self, parent, wallet, msg, kind):
|
def __init__(self, parent, wallet):
|
||||||
WindowModalDialog.__init__(self, parent)
|
WindowModalDialog.__init__(self, parent)
|
||||||
|
is_encrypted = wallet.storage.is_encrypted()
|
||||||
|
if not wallet.has_password():
|
||||||
|
msg = _('Your wallet is not protected.')
|
||||||
|
msg += ' ' + _('Use this dialog to add a password to your wallet.')
|
||||||
|
else:
|
||||||
|
if not is_encrypted:
|
||||||
|
msg = _('Your bitcoins are password protected. However, your wallet file is not encrypted.')
|
||||||
|
else:
|
||||||
|
msg = _('Your wallet is password protected and encrypted.')
|
||||||
|
msg += ' ' + _('Use this dialog to change your password.')
|
||||||
OK_button = OkButton(self)
|
OK_button = OkButton(self)
|
||||||
self.playout = PasswordLayout(wallet, msg, kind, OK_button)
|
self.playout = PasswordLayout(wallet, msg, PW_CHANGE, OK_button)
|
||||||
self.setWindowTitle(self.playout.title())
|
self.setWindowTitle(self.playout.title())
|
||||||
vbox = QVBoxLayout(self)
|
vbox = QVBoxLayout(self)
|
||||||
vbox.addLayout(self.playout.layout())
|
vbox.addLayout(self.playout.layout())
|
||||||
vbox.addStretch(1)
|
vbox.addStretch(1)
|
||||||
vbox.addLayout(Buttons(CancelButton(self), OK_button))
|
vbox.addLayout(Buttons(CancelButton(self), OK_button))
|
||||||
|
self.playout.encrypt_cb.setChecked(is_encrypted or not wallet.has_password())
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
if not self.exec_():
|
if not self.exec_():
|
||||||
return False, None, None
|
return False, None, None, None
|
||||||
|
return True, self.playout.old_password(), self.playout.new_password(), self.playout.encrypt_cb.isChecked()
|
||||||
|
|
||||||
return True, self.playout.old_password(), self.playout.new_password()
|
|
||||||
|
class PasswordDialog(WindowModalDialog):
|
||||||
|
|
||||||
|
def __init__(self, parent=None, msg=None):
|
||||||
|
msg = msg or _('Please enter your password')
|
||||||
|
WindowModalDialog.__init__(self, parent, _("Enter Password"))
|
||||||
|
self.pw = pw = QLineEdit()
|
||||||
|
pw.setEchoMode(2)
|
||||||
|
vbox = QVBoxLayout()
|
||||||
|
vbox.addWidget(QLabel(msg))
|
||||||
|
grid = QGridLayout()
|
||||||
|
grid.setSpacing(8)
|
||||||
|
grid.addWidget(QLabel(_('Password')), 1, 0)
|
||||||
|
grid.addWidget(pw, 1, 1)
|
||||||
|
vbox.addLayout(grid)
|
||||||
|
vbox.addLayout(Buttons(CancelButton(self), OkButton(self)))
|
||||||
|
self.setLayout(vbox)
|
||||||
|
run_hook('password_dialog', pw, grid, 1)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
if not self.exec_():
|
||||||
|
return
|
||||||
|
return unicode(self.pw.text())
|
||||||
|
|
|
@ -19,6 +19,8 @@ class ElectrumGui:
|
||||||
if not storage.file_exists:
|
if not storage.file_exists:
|
||||||
print "Wallet not found. try 'electrum create'"
|
print "Wallet not found. try 'electrum create'"
|
||||||
exit()
|
exit()
|
||||||
|
password = getpass.getpass('Password:', stream=None) if storage.is_encrypted() else None
|
||||||
|
storage.read(password)
|
||||||
|
|
||||||
self.done = 0
|
self.done = 0
|
||||||
self.last_balance = ""
|
self.last_balance = ""
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import tty, sys
|
import tty, sys
|
||||||
import curses, datetime, locale
|
import curses, datetime, locale
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
import getpass
|
||||||
|
|
||||||
from electrum.util import format_satoshis, set_verbosity
|
from electrum.util import format_satoshis, set_verbosity
|
||||||
from electrum.util import StoreDict
|
from electrum.util import StoreDict
|
||||||
|
@ -21,7 +22,8 @@ class ElectrumGui:
|
||||||
if not storage.file_exists:
|
if not storage.file_exists:
|
||||||
print "Wallet not found. try 'electrum create'"
|
print "Wallet not found. try 'electrum create'"
|
||||||
exit()
|
exit()
|
||||||
|
password = getpass.getpass('Password:', stream=None) if storage.is_encrypted() else None
|
||||||
|
storage.read(password)
|
||||||
self.wallet = Wallet(storage)
|
self.wallet = Wallet(storage)
|
||||||
self.wallet.start_threads(self.network)
|
self.wallet.start_threads(self.network)
|
||||||
self.contacts = StoreDict(self.config, 'contacts')
|
self.contacts = StoreDict(self.config, 'contacts')
|
||||||
|
|
|
@ -331,8 +331,8 @@ class BaseWizard(object):
|
||||||
else:
|
else:
|
||||||
self.on_password(None)
|
self.on_password(None)
|
||||||
|
|
||||||
def on_password(self, password):
|
def on_password(self, password, encrypt):
|
||||||
self.storage.put('use_encryption', bool(password))
|
self.storage.set_password(password, encrypt)
|
||||||
for k in self.keystores:
|
for k in self.keystores:
|
||||||
if k.may_have_password():
|
if k.may_have_password():
|
||||||
k.update_password(None, password)
|
k.update_password(None, password)
|
||||||
|
|
|
@ -653,34 +653,26 @@ class EC_KEY(object):
|
||||||
|
|
||||||
|
|
||||||
def decrypt_message(self, encrypted):
|
def decrypt_message(self, encrypted):
|
||||||
|
|
||||||
encrypted = base64.b64decode(encrypted)
|
encrypted = base64.b64decode(encrypted)
|
||||||
|
|
||||||
if len(encrypted) < 85:
|
if len(encrypted) < 85:
|
||||||
raise Exception('invalid ciphertext: length')
|
raise Exception('invalid ciphertext: length')
|
||||||
|
|
||||||
magic = encrypted[:4]
|
magic = encrypted[:4]
|
||||||
ephemeral_pubkey = encrypted[4:37]
|
ephemeral_pubkey = encrypted[4:37]
|
||||||
ciphertext = encrypted[37:-32]
|
ciphertext = encrypted[37:-32]
|
||||||
mac = encrypted[-32:]
|
mac = encrypted[-32:]
|
||||||
|
|
||||||
if magic != 'BIE1':
|
if magic != 'BIE1':
|
||||||
raise Exception('invalid ciphertext: invalid magic bytes')
|
raise Exception('invalid ciphertext: invalid magic bytes')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ephemeral_pubkey = ser_to_point(ephemeral_pubkey)
|
ephemeral_pubkey = ser_to_point(ephemeral_pubkey)
|
||||||
except AssertionError, e:
|
except AssertionError, e:
|
||||||
raise Exception('invalid ciphertext: invalid ephemeral pubkey')
|
raise Exception('invalid ciphertext: invalid ephemeral pubkey')
|
||||||
|
|
||||||
if not ecdsa.ecdsa.point_is_valid(generator_secp256k1, ephemeral_pubkey.x(), ephemeral_pubkey.y()):
|
if not ecdsa.ecdsa.point_is_valid(generator_secp256k1, ephemeral_pubkey.x(), ephemeral_pubkey.y()):
|
||||||
raise Exception('invalid ciphertext: invalid ephemeral pubkey')
|
raise Exception('invalid ciphertext: invalid ephemeral pubkey')
|
||||||
|
|
||||||
ecdh_key = point_to_ser(ephemeral_pubkey * self.privkey.secret_multiplier)
|
ecdh_key = point_to_ser(ephemeral_pubkey * self.privkey.secret_multiplier)
|
||||||
key = hashlib.sha512(ecdh_key).digest()
|
key = hashlib.sha512(ecdh_key).digest()
|
||||||
iv, key_e, key_m = key[0:16], key[16:32], key[32:]
|
iv, key_e, key_m = key[0:16], key[16:32], key[32:]
|
||||||
if mac != hmac.new(key_m, encrypted[:-32], hashlib.sha256).digest():
|
if mac != hmac.new(key_m, encrypted[:-32], hashlib.sha256).digest():
|
||||||
raise Exception('invalid ciphertext: invalid mac')
|
raise InvalidPassword()
|
||||||
|
|
||||||
return aes_decrypt_with_iv(key_e, iv, ciphertext)
|
return aes_decrypt_with_iv(key_e, iv, ciphertext)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -796,7 +796,7 @@ def get_parser():
|
||||||
add_global_options(parser_gui)
|
add_global_options(parser_gui)
|
||||||
# daemon
|
# daemon
|
||||||
parser_daemon = subparsers.add_parser('daemon', help="Run Daemon")
|
parser_daemon = subparsers.add_parser('daemon', help="Run Daemon")
|
||||||
parser_daemon.add_argument("subcommand", choices=['start', 'status', 'stop'], nargs='?')
|
parser_daemon.add_argument("subcommand", choices=['start', 'status', 'stop', 'open', 'close'], nargs='?')
|
||||||
#parser_daemon.set_defaults(func=run_daemon)
|
#parser_daemon.set_defaults(func=run_daemon)
|
||||||
add_network_options(parser_daemon)
|
add_network_options(parser_daemon)
|
||||||
add_global_options(parser_daemon)
|
add_global_options(parser_daemon)
|
||||||
|
|
|
@ -34,7 +34,7 @@ from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer, SimpleJSONRPCReq
|
||||||
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
|
||||||
from util import print_msg, print_error, print_stderr
|
from util import print_msg, print_error, print_stderr, UserCancelled
|
||||||
from wallet import WalletStorage, Wallet
|
from wallet import WalletStorage, Wallet
|
||||||
from commands import known_commands, Commands
|
from commands import known_commands, Commands
|
||||||
from simple_config import SimpleConfig
|
from simple_config import SimpleConfig
|
||||||
|
@ -115,8 +115,7 @@ class Daemon(DaemonThread):
|
||||||
self.gui = None
|
self.gui = None
|
||||||
self.wallets = {}
|
self.wallets = {}
|
||||||
# Setup JSONRPC server
|
# Setup JSONRPC server
|
||||||
path = config.get_wallet_path()
|
default_wallet = None
|
||||||
default_wallet = self.load_wallet(path)
|
|
||||||
self.cmd_runner = Commands(self.config, default_wallet, self.network)
|
self.cmd_runner = Commands(self.config, default_wallet, self.network)
|
||||||
self.init_server(config, fd)
|
self.init_server(config, fd)
|
||||||
|
|
||||||
|
@ -145,11 +144,24 @@ class Daemon(DaemonThread):
|
||||||
def ping(self):
|
def ping(self):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def run_daemon(self, config):
|
def run_daemon(self, config_options):
|
||||||
|
config = SimpleConfig(config_options)
|
||||||
sub = config.get('subcommand')
|
sub = config.get('subcommand')
|
||||||
assert sub in [None, 'start', 'stop', 'status']
|
assert sub in [None, 'start', 'stop', 'status', 'open', 'close']
|
||||||
if sub in [None, 'start']:
|
if sub in [None, 'start']:
|
||||||
response = "Daemon already running"
|
response = "Daemon already running"
|
||||||
|
elif sub == 'open':
|
||||||
|
path = config.get_wallet_path()
|
||||||
|
self.load_wallet(path, lambda: config.get('password'))
|
||||||
|
response = True
|
||||||
|
elif sub == 'close':
|
||||||
|
path = config.get_wallet_path()
|
||||||
|
if path in self.wallets:
|
||||||
|
wallet = self.wallets.pop(path)
|
||||||
|
wallet.stop_threads()
|
||||||
|
response = True
|
||||||
|
else:
|
||||||
|
response = False
|
||||||
elif sub == 'status':
|
elif sub == 'status':
|
||||||
if self.network:
|
if self.network:
|
||||||
p = self.network.get_parameters()
|
p = self.network.get_parameters()
|
||||||
|
@ -185,7 +197,7 @@ class Daemon(DaemonThread):
|
||||||
response = "Error: Electrum is running in daemon mode. Please stop the daemon first."
|
response = "Error: Electrum is running in daemon mode. Please stop the daemon first."
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def load_wallet(self, path):
|
def load_wallet(self, path, password_getter):
|
||||||
# wizard will be launched if we return
|
# wizard will be launched if we return
|
||||||
if path in self.wallets:
|
if path in self.wallets:
|
||||||
wallet = self.wallets[path]
|
wallet = self.wallets[path]
|
||||||
|
@ -193,6 +205,13 @@ class Daemon(DaemonThread):
|
||||||
storage = WalletStorage(path)
|
storage = WalletStorage(path)
|
||||||
if not storage.file_exists:
|
if not storage.file_exists:
|
||||||
return
|
return
|
||||||
|
if storage.is_encrypted():
|
||||||
|
password = password_getter()
|
||||||
|
if not password:
|
||||||
|
raise UserCancelled()
|
||||||
|
else:
|
||||||
|
password = None
|
||||||
|
storage.read(password)
|
||||||
if storage.requires_split():
|
if storage.requires_split():
|
||||||
return
|
return
|
||||||
if storage.requires_upgrade():
|
if storage.requires_upgrade():
|
||||||
|
@ -214,20 +233,25 @@ class Daemon(DaemonThread):
|
||||||
wallet.stop_threads()
|
wallet.stop_threads()
|
||||||
|
|
||||||
def run_cmdline(self, config_options):
|
def run_cmdline(self, config_options):
|
||||||
|
password = config_options.get('password')
|
||||||
|
new_password = config_options.get('new_password')
|
||||||
config = SimpleConfig(config_options)
|
config = SimpleConfig(config_options)
|
||||||
cmdname = config.get('cmd')
|
cmdname = config.get('cmd')
|
||||||
cmd = known_commands[cmdname]
|
cmd = known_commands[cmdname]
|
||||||
path = config.get_wallet_path()
|
if cmd.requires_wallet:
|
||||||
wallet = self.load_wallet(path) if cmd.requires_wallet else None
|
path = config.get_wallet_path()
|
||||||
|
wallet = self.wallets.get(path)
|
||||||
|
if wallet is None:
|
||||||
|
return {'error': 'Wallet not open. Use "electrum daemon open -w wallet"'}
|
||||||
|
else:
|
||||||
|
wallet = None
|
||||||
# arguments passed to function
|
# arguments passed to function
|
||||||
args = map(lambda x: config.get(x), cmd.params)
|
args = map(lambda x: config.get(x), cmd.params)
|
||||||
# decode json arguments
|
# decode json arguments
|
||||||
args = map(json_decode, args)
|
args = map(json_decode, args)
|
||||||
# options
|
# options
|
||||||
args += map(lambda x: config.get(x), cmd.options)
|
args += map(lambda x: config.get(x), cmd.options)
|
||||||
cmd_runner = Commands(config, wallet, self.network,
|
cmd_runner = Commands(config, wallet, self.network, password=password, new_password=new_password)
|
||||||
password=config_options.get('password'),
|
|
||||||
new_password=config_options.get('new_password'))
|
|
||||||
func = getattr(cmd_runner, cmd.name)
|
func = getattr(cmd_runner, cmd.name)
|
||||||
result = func(*args)
|
result = func(*args)
|
||||||
return result
|
return result
|
||||||
|
|
|
@ -32,11 +32,15 @@ import json
|
||||||
import copy
|
import copy
|
||||||
import re
|
import re
|
||||||
import stat
|
import stat
|
||||||
|
import pbkdf2, hmac, hashlib
|
||||||
|
import base64
|
||||||
|
import zlib
|
||||||
|
|
||||||
from i18n import _
|
from i18n import _
|
||||||
from util import NotEnoughFunds, PrintError, profiler
|
from util import NotEnoughFunds, PrintError, profiler
|
||||||
from plugins import run_hook, plugin_loaders
|
from plugins import run_hook, plugin_loaders
|
||||||
from keystore import bip44_derivation
|
from keystore import bip44_derivation
|
||||||
|
import bitcoin
|
||||||
|
|
||||||
|
|
||||||
# seed_version is now used for the version of the wallet file
|
# seed_version is now used for the version of the wallet file
|
||||||
|
@ -63,50 +67,57 @@ class WalletStorage(PrintError):
|
||||||
self.lock = threading.RLock()
|
self.lock = threading.RLock()
|
||||||
self.data = {}
|
self.data = {}
|
||||||
self.path = path
|
self.path = path
|
||||||
self.file_exists = False
|
self.file_exists = os.path.exists(self.path)
|
||||||
self.modified = False
|
self.modified = False
|
||||||
self.print_error("wallet path", self.path)
|
self.pubkey = None
|
||||||
if self.path:
|
|
||||||
self.read(self.path)
|
|
||||||
# check here if I need to load a plugin
|
# check here if I need to load a plugin
|
||||||
t = self.get('wallet_type')
|
t = self.get('wallet_type')
|
||||||
l = plugin_loaders.get(t)
|
l = plugin_loaders.get(t)
|
||||||
if l: l()
|
if l: l()
|
||||||
|
|
||||||
|
def decrypt(self, s, password):
|
||||||
|
# Note: hardware wallets should use a seed-derived key and not require a password.
|
||||||
|
# Thus, we need to expose keystore metadata
|
||||||
|
if password is None:
|
||||||
|
self.pubkey = None
|
||||||
|
return s
|
||||||
|
secret = pbkdf2.PBKDF2(password, '', iterations = 1024, macmodule = hmac, digestmodule = hashlib.sha512).read(64)
|
||||||
|
ec_key = bitcoin.EC_KEY(secret)
|
||||||
|
self.pubkey = ec_key.get_public_key()
|
||||||
|
return zlib.decompress(ec_key.decrypt_message(s)) if s else None
|
||||||
|
|
||||||
def read(self, path):
|
def set_password(self, pw, encrypt):
|
||||||
"""Read the contents of the wallet file."""
|
"""Set self.pubkey"""
|
||||||
|
self.put('use_encryption', (pw is not None))
|
||||||
|
self.decrypt(None, pw if encrypt else None)
|
||||||
|
|
||||||
|
def is_encrypted(self):
|
||||||
try:
|
try:
|
||||||
with open(self.path, "r") as f:
|
with open(self.path, "r") as f:
|
||||||
data = f.read()
|
s = f.read(8)
|
||||||
except IOError:
|
except IOError:
|
||||||
return
|
return
|
||||||
if not data:
|
|
||||||
return
|
|
||||||
try:
|
try:
|
||||||
self.data = json.loads(data)
|
return base64.b64decode(s).startswith('BIE1')
|
||||||
except:
|
except:
|
||||||
try:
|
return False
|
||||||
d = ast.literal_eval(data) #parse raw data from reading wallet file
|
|
||||||
labels = d.get('labels', {})
|
def read(self, password):
|
||||||
except Exception as e:
|
"""Read the contents of the wallet file."""
|
||||||
raise IOError("Cannot read wallet file '%s'" % self.path)
|
self.print_error("wallet path", self.path)
|
||||||
self.data = {}
|
try:
|
||||||
# In old versions of Electrum labels were latin1 encoded, this fixes breakage.
|
with open(self.path, "r") as f:
|
||||||
for i, label in labels.items():
|
s = f.read()
|
||||||
try:
|
except IOError:
|
||||||
unicode(label)
|
return
|
||||||
except UnicodeDecodeError:
|
if not s:
|
||||||
d['labels'][i] = unicode(label.decode('latin1'))
|
return
|
||||||
for key, value in d.items():
|
# Decrypt wallet.
|
||||||
try:
|
s = self.decrypt(s, password)
|
||||||
json.dumps(key)
|
try:
|
||||||
json.dumps(value)
|
self.data = json.loads(s)
|
||||||
except:
|
except:
|
||||||
self.print_error('Failed to convert label to json format', key)
|
raise IOError("Cannot read wallet file '%s'" % self.path)
|
||||||
continue
|
|
||||||
self.data[key] = value
|
|
||||||
self.file_exists = True
|
|
||||||
|
|
||||||
def get(self, key, default=None):
|
def get(self, key, default=None):
|
||||||
with self.lock:
|
with self.lock:
|
||||||
|
@ -133,6 +144,7 @@ class WalletStorage(PrintError):
|
||||||
self.modified = True
|
self.modified = True
|
||||||
self.data.pop(key)
|
self.data.pop(key)
|
||||||
|
|
||||||
|
@profiler
|
||||||
def write(self):
|
def write(self):
|
||||||
# this ensures that previous versions of electrum won't open the wallet
|
# this ensures that previous versions of electrum won't open the wallet
|
||||||
self.put('seed_version', FINAL_SEED_VERSION)
|
self.put('seed_version', FINAL_SEED_VERSION)
|
||||||
|
@ -147,6 +159,9 @@ class WalletStorage(PrintError):
|
||||||
if not self.modified:
|
if not self.modified:
|
||||||
return
|
return
|
||||||
s = json.dumps(self.data, indent=4, sort_keys=True)
|
s = json.dumps(self.data, indent=4, sort_keys=True)
|
||||||
|
if self.pubkey:
|
||||||
|
s = bitcoin.encrypt_message(zlib.compress(s), self.pubkey)
|
||||||
|
|
||||||
temp_path = "%s.tmp.%s" % (self.path, os.getpid())
|
temp_path = "%s.tmp.%s" % (self.path, os.getpid())
|
||||||
with open(temp_path, "w") as f:
|
with open(temp_path, "w") as f:
|
||||||
f.write(s)
|
f.write(s)
|
||||||
|
|
|
@ -1577,10 +1577,10 @@ class Simple_Deterministic_Wallet(Deterministic_Wallet, Simple_Wallet):
|
||||||
def check_password(self, password):
|
def check_password(self, password):
|
||||||
self.keystore.check_password(password)
|
self.keystore.check_password(password)
|
||||||
|
|
||||||
def update_password(self, old_pw, new_pw):
|
def update_password(self, old_pw, new_pw, encrypt=False):
|
||||||
self.keystore.update_password(old_pw, new_pw)
|
self.keystore.update_password(old_pw, new_pw)
|
||||||
self.save_keystore()
|
self.save_keystore()
|
||||||
self.storage.put('use_encryption', (new_pw is not None))
|
self.storage.set_password(new_pw, encrypt)
|
||||||
self.storage.write()
|
self.storage.write()
|
||||||
|
|
||||||
def save_keystore(self):
|
def save_keystore(self):
|
||||||
|
@ -1686,7 +1686,7 @@ class Multisig_Wallet(Deterministic_Wallet, P2SH):
|
||||||
if keystore.can_change_password():
|
if keystore.can_change_password():
|
||||||
keystore.update_password(old_pw, new_pw)
|
keystore.update_password(old_pw, new_pw)
|
||||||
self.storage.put(name, keystore.dump())
|
self.storage.put(name, keystore.dump())
|
||||||
self.storage.put('use_encryption', (new_pw is not None))
|
self.storage.set_password(new_pw)
|
||||||
|
|
||||||
def check_password(self, password):
|
def check_password(self, password):
|
||||||
self.keystore.check_password(password)
|
self.keystore.check_password(password)
|
||||||
|
|
Loading…
Reference in New Issue