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:
@@ -331,8 +331,8 @@ class BaseWizard(object):
|
||||
else:
|
||||
self.on_password(None)
|
||||
|
||||
def on_password(self, password):
|
||||
self.storage.put('use_encryption', bool(password))
|
||||
def on_password(self, password, encrypt):
|
||||
self.storage.set_password(password, encrypt)
|
||||
for k in self.keystores:
|
||||
if k.may_have_password():
|
||||
k.update_password(None, password)
|
||||
|
||||
@@ -653,34 +653,26 @@ class EC_KEY(object):
|
||||
|
||||
|
||||
def decrypt_message(self, encrypted):
|
||||
|
||||
encrypted = base64.b64decode(encrypted)
|
||||
|
||||
if len(encrypted) < 85:
|
||||
raise Exception('invalid ciphertext: length')
|
||||
|
||||
magic = encrypted[:4]
|
||||
ephemeral_pubkey = encrypted[4:37]
|
||||
ciphertext = encrypted[37:-32]
|
||||
mac = encrypted[-32:]
|
||||
|
||||
if magic != 'BIE1':
|
||||
raise Exception('invalid ciphertext: invalid magic bytes')
|
||||
|
||||
try:
|
||||
ephemeral_pubkey = ser_to_point(ephemeral_pubkey)
|
||||
except AssertionError, e:
|
||||
raise Exception('invalid ciphertext: invalid ephemeral pubkey')
|
||||
|
||||
if not ecdsa.ecdsa.point_is_valid(generator_secp256k1, ephemeral_pubkey.x(), ephemeral_pubkey.y()):
|
||||
raise Exception('invalid ciphertext: invalid ephemeral pubkey')
|
||||
|
||||
ecdh_key = point_to_ser(ephemeral_pubkey * self.privkey.secret_multiplier)
|
||||
key = hashlib.sha512(ecdh_key).digest()
|
||||
iv, key_e, key_m = key[0:16], key[16:32], key[32:]
|
||||
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)
|
||||
|
||||
|
||||
|
||||
@@ -796,7 +796,7 @@ def get_parser():
|
||||
add_global_options(parser_gui)
|
||||
# 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)
|
||||
add_network_options(parser_daemon)
|
||||
add_global_options(parser_daemon)
|
||||
|
||||
@@ -34,7 +34,7 @@ from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer, SimpleJSONRPCReq
|
||||
from version import ELECTRUM_VERSION
|
||||
from network import Network
|
||||
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 commands import known_commands, Commands
|
||||
from simple_config import SimpleConfig
|
||||
@@ -115,8 +115,7 @@ class Daemon(DaemonThread):
|
||||
self.gui = None
|
||||
self.wallets = {}
|
||||
# Setup JSONRPC server
|
||||
path = config.get_wallet_path()
|
||||
default_wallet = self.load_wallet(path)
|
||||
default_wallet = None
|
||||
self.cmd_runner = Commands(self.config, default_wallet, self.network)
|
||||
self.init_server(config, fd)
|
||||
|
||||
@@ -145,11 +144,24 @@ class Daemon(DaemonThread):
|
||||
def ping(self):
|
||||
return True
|
||||
|
||||
def run_daemon(self, config):
|
||||
def run_daemon(self, config_options):
|
||||
config = SimpleConfig(config_options)
|
||||
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']:
|
||||
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':
|
||||
if self.network:
|
||||
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."
|
||||
return response
|
||||
|
||||
def load_wallet(self, path):
|
||||
def load_wallet(self, path, password_getter):
|
||||
# wizard will be launched if we return
|
||||
if path in self.wallets:
|
||||
wallet = self.wallets[path]
|
||||
@@ -193,6 +205,13 @@ class Daemon(DaemonThread):
|
||||
storage = WalletStorage(path)
|
||||
if not storage.file_exists:
|
||||
return
|
||||
if storage.is_encrypted():
|
||||
password = password_getter()
|
||||
if not password:
|
||||
raise UserCancelled()
|
||||
else:
|
||||
password = None
|
||||
storage.read(password)
|
||||
if storage.requires_split():
|
||||
return
|
||||
if storage.requires_upgrade():
|
||||
@@ -214,20 +233,25 @@ class Daemon(DaemonThread):
|
||||
wallet.stop_threads()
|
||||
|
||||
def run_cmdline(self, config_options):
|
||||
password = config_options.get('password')
|
||||
new_password = config_options.get('new_password')
|
||||
config = SimpleConfig(config_options)
|
||||
cmdname = config.get('cmd')
|
||||
cmd = known_commands[cmdname]
|
||||
path = config.get_wallet_path()
|
||||
wallet = self.load_wallet(path) if cmd.requires_wallet else None
|
||||
if cmd.requires_wallet:
|
||||
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
|
||||
args = map(lambda x: config.get(x), cmd.params)
|
||||
# decode json arguments
|
||||
args = map(json_decode, args)
|
||||
# options
|
||||
args += map(lambda x: config.get(x), cmd.options)
|
||||
cmd_runner = Commands(config, wallet, self.network,
|
||||
password=config_options.get('password'),
|
||||
new_password=config_options.get('new_password'))
|
||||
cmd_runner = Commands(config, wallet, self.network, password=password, new_password=new_password)
|
||||
func = getattr(cmd_runner, cmd.name)
|
||||
result = func(*args)
|
||||
return result
|
||||
|
||||
@@ -32,11 +32,15 @@ import json
|
||||
import copy
|
||||
import re
|
||||
import stat
|
||||
import pbkdf2, hmac, hashlib
|
||||
import base64
|
||||
import zlib
|
||||
|
||||
from i18n import _
|
||||
from util import NotEnoughFunds, PrintError, profiler
|
||||
from plugins import run_hook, plugin_loaders
|
||||
from keystore import bip44_derivation
|
||||
import bitcoin
|
||||
|
||||
|
||||
# seed_version is now used for the version of the wallet file
|
||||
@@ -63,50 +67,57 @@ class WalletStorage(PrintError):
|
||||
self.lock = threading.RLock()
|
||||
self.data = {}
|
||||
self.path = path
|
||||
self.file_exists = False
|
||||
self.file_exists = os.path.exists(self.path)
|
||||
self.modified = False
|
||||
self.print_error("wallet path", self.path)
|
||||
if self.path:
|
||||
self.read(self.path)
|
||||
self.pubkey = None
|
||||
# check here if I need to load a plugin
|
||||
t = self.get('wallet_type')
|
||||
l = plugin_loaders.get(t)
|
||||
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):
|
||||
"""Read the contents of the wallet file."""
|
||||
def set_password(self, pw, encrypt):
|
||||
"""Set self.pubkey"""
|
||||
self.put('use_encryption', (pw is not None))
|
||||
self.decrypt(None, pw if encrypt else None)
|
||||
|
||||
def is_encrypted(self):
|
||||
try:
|
||||
with open(self.path, "r") as f:
|
||||
data = f.read()
|
||||
s = f.read(8)
|
||||
except IOError:
|
||||
return
|
||||
if not data:
|
||||
return
|
||||
try:
|
||||
self.data = json.loads(data)
|
||||
return base64.b64decode(s).startswith('BIE1')
|
||||
except:
|
||||
try:
|
||||
d = ast.literal_eval(data) #parse raw data from reading wallet file
|
||||
labels = d.get('labels', {})
|
||||
except Exception as e:
|
||||
raise IOError("Cannot read wallet file '%s'" % self.path)
|
||||
self.data = {}
|
||||
# In old versions of Electrum labels were latin1 encoded, this fixes breakage.
|
||||
for i, label in labels.items():
|
||||
try:
|
||||
unicode(label)
|
||||
except UnicodeDecodeError:
|
||||
d['labels'][i] = unicode(label.decode('latin1'))
|
||||
for key, value in d.items():
|
||||
try:
|
||||
json.dumps(key)
|
||||
json.dumps(value)
|
||||
except:
|
||||
self.print_error('Failed to convert label to json format', key)
|
||||
continue
|
||||
self.data[key] = value
|
||||
self.file_exists = True
|
||||
return False
|
||||
|
||||
def read(self, password):
|
||||
"""Read the contents of the wallet file."""
|
||||
self.print_error("wallet path", self.path)
|
||||
try:
|
||||
with open(self.path, "r") as f:
|
||||
s = f.read()
|
||||
except IOError:
|
||||
return
|
||||
if not s:
|
||||
return
|
||||
# Decrypt wallet.
|
||||
s = self.decrypt(s, password)
|
||||
try:
|
||||
self.data = json.loads(s)
|
||||
except:
|
||||
raise IOError("Cannot read wallet file '%s'" % self.path)
|
||||
|
||||
def get(self, key, default=None):
|
||||
with self.lock:
|
||||
@@ -133,6 +144,7 @@ class WalletStorage(PrintError):
|
||||
self.modified = True
|
||||
self.data.pop(key)
|
||||
|
||||
@profiler
|
||||
def write(self):
|
||||
# this ensures that previous versions of electrum won't open the wallet
|
||||
self.put('seed_version', FINAL_SEED_VERSION)
|
||||
@@ -147,6 +159,9 @@ class WalletStorage(PrintError):
|
||||
if not self.modified:
|
||||
return
|
||||
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())
|
||||
with open(temp_path, "w") as f:
|
||||
f.write(s)
|
||||
|
||||
@@ -1577,10 +1577,10 @@ class Simple_Deterministic_Wallet(Deterministic_Wallet, Simple_Wallet):
|
||||
def check_password(self, 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.save_keystore()
|
||||
self.storage.put('use_encryption', (new_pw is not None))
|
||||
self.storage.set_password(new_pw, encrypt)
|
||||
self.storage.write()
|
||||
|
||||
def save_keystore(self):
|
||||
@@ -1686,7 +1686,7 @@ class Multisig_Wallet(Deterministic_Wallet, P2SH):
|
||||
if keystore.can_change_password():
|
||||
keystore.update_password(old_pw, new_pw)
|
||||
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):
|
||||
self.keystore.check_password(password)
|
||||
|
||||
Reference in New Issue
Block a user