Wallet encryption (#783)
* update known commands * add wallet_unlock, block wallet startup on being unlocked * add wallet_decrypt and wallet_encrypt * wallet encryption unit tests * added use_keyring configuration option in order to make keyring password storage optional
This commit is contained in:
parent
bcdeea75d6
commit
8c2d381aee
6 changed files with 227 additions and 18 deletions
14
CHANGELOG.md
14
CHANGELOG.md
|
@ -34,8 +34,20 @@ at anytime.
|
|||
* Don't include file names when logging information about streams, only include sd hashes
|
||||
* Re-use addresses used for lbrycrd info exchange, this was a significant source of address bloat in the wallet
|
||||
* Remove manual saving of the wallet in from lbrynet, let lbryum handle it
|
||||
* Block wallet startup on being unlocked if it is encrypted
|
||||
|
||||
### Added
|
||||
* Add link to instructions on how to change the default peer port
|
||||
* Add `peer_port` to settings configurable using `settings_set`
|
||||
* Added an option to disable max key fee check.
|
||||
* Add `wallet_unlock`, a command available during startup to unlock an encrypted wallet
|
||||
|
||||
### Changed
|
||||
* claim_show API command no longer takes name as argument
|
||||
* Linux default downloads folder changed from `~/Downloads` to `XDG_DOWNLOAD_DIR`
|
||||
* Linux folders moved from the home directory to `~/.local/share/lbry`
|
||||
* Windows folders moved from `%AppData%/Roaming` to `%AppData%/Local/lbry`
|
||||
* Block wallet startup on being unlocked
|
||||
* Added `status`, `blobs_completed`, and `blobs_in_stream` fields to file objects returned by `file_list` and `get`
|
||||
* Added `channel_import` and `channel_export` commands
|
||||
* Added `is_mine` field to `channel_list` results
|
||||
|
@ -43,6 +55,8 @@ at anytime.
|
|||
* Added user configurable `auto_renew_claim_height_delta` setting, defaults to 0 (off)
|
||||
* Added `lbrynet-console`, a tool to run or connect to lbrynet-daemon and launch an interactive python console with the api functions built in.
|
||||
* Added a table to the lbry file database to store the outpoint of the claim downloaded from
|
||||
* Added `wallet_unlock`, a command available during startup to unlock an encrypted wallet
|
||||
* Added support for wallet encryption via new commands `wallet_decrypt` and `wallet_encrypt`
|
||||
|
||||
### Removed
|
||||
* Removed claim related filter arguments `name`, `claim_id`, and `outpoint` from `file_list`, `file_delete`, `file_set_status`, and `file_reflect`
|
||||
|
|
|
@ -282,6 +282,7 @@ ADJUSTABLE_SETTINGS = {
|
|||
'peer_search_timeout': (int, 3),
|
||||
'use_auth_http': (bool, False),
|
||||
'use_upnp': (bool, True),
|
||||
'use_keyring': (bool, False),
|
||||
'wallet': (str, LBRYUM_WALLET),
|
||||
}
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ from lbryum.network import Network
|
|||
from lbryum.simple_config import SimpleConfig
|
||||
from lbryum.constants import COIN
|
||||
from lbryum.commands import Commands
|
||||
from lbryum.errors import InvalidPassword
|
||||
|
||||
from lbryschema.uri import parse_lbry_uri
|
||||
from lbryschema.claim import ClaimDict
|
||||
|
@ -1158,6 +1159,12 @@ class Wallet(object):
|
|||
def get_least_used_address(self, account=None, for_change=False, max_count=100):
|
||||
return defer.fail(NotImplementedError())
|
||||
|
||||
def decrypt_wallet(self):
|
||||
return defer.fail(NotImplementedError())
|
||||
|
||||
def encrypt_wallet(self, new_password, update_keyring=False):
|
||||
return defer.fail(NotImplementedError())
|
||||
|
||||
def _start(self):
|
||||
pass
|
||||
|
||||
|
@ -1172,6 +1179,9 @@ class LBRYumWallet(Wallet):
|
|||
self.config = make_config(self._config)
|
||||
self.network = None
|
||||
self.wallet = None
|
||||
self._cmd_runner = None
|
||||
self.wallet_pw_d = None
|
||||
self.wallet_unlocked_d = defer.Deferred()
|
||||
self.is_first_run = False
|
||||
self.printed_retrieving_headers = False
|
||||
self._start_check = None
|
||||
|
@ -1185,6 +1195,12 @@ class LBRYumWallet(Wallet):
|
|||
return (not self.printed_retrieving_headers and
|
||||
self.network.blockchain.retrieving_headers)
|
||||
|
||||
def get_cmd_runner(self):
|
||||
if self._cmd_runner is None:
|
||||
self._cmd_runner = Commands(self.config, self.wallet, self.network)
|
||||
|
||||
return self._cmd_runner
|
||||
|
||||
def _start(self):
|
||||
network_start_d = defer.Deferred()
|
||||
|
||||
|
@ -1206,6 +1222,34 @@ class LBRYumWallet(Wallet):
|
|||
else:
|
||||
network_start_d.errback(ValueError("Failed to connect to network."))
|
||||
|
||||
def unlock(password):
|
||||
if self._cmd_runner and self._cmd_runner.locked:
|
||||
try:
|
||||
self._cmd_runner.unlock_wallet(password)
|
||||
except InvalidPassword:
|
||||
log.warning("Incorrect password")
|
||||
check_locked()
|
||||
raise InvalidPassword
|
||||
if self._cmd_runner and self._cmd_runner.locked:
|
||||
raise Exception("Failed to unlock wallet")
|
||||
elif not self._cmd_runner:
|
||||
raise Exception("Command runner hasn't been initialized yet")
|
||||
self.wallet_unlocked_d.callback(True)
|
||||
log.info("Unlocked the wallet!")
|
||||
|
||||
def check_locked():
|
||||
if self._cmd_runner and self._cmd_runner.locked:
|
||||
log.info("Waiting for wallet password")
|
||||
d = defer.Deferred()
|
||||
d.addCallback(unlock)
|
||||
self.wallet_pw_d = d
|
||||
return self.wallet_unlocked_d
|
||||
elif not self._cmd_runner:
|
||||
raise Exception("Command runner hasn't been initialized yet")
|
||||
if not self.wallet.use_encryption:
|
||||
log.info("Wallet is not encrypted")
|
||||
self.wallet_unlocked_d.callback(True)
|
||||
|
||||
self._start_check = task.LoopingCall(check_started)
|
||||
|
||||
d = setup_network()
|
||||
|
@ -1216,6 +1260,9 @@ class LBRYumWallet(Wallet):
|
|||
d.addCallback(lambda _: log.info("Subscribing to addresses"))
|
||||
d.addCallback(lambda _: self.wallet.wait_until_synchronized(lambda _: None))
|
||||
d.addCallback(lambda _: log.info("Synchronized wallet"))
|
||||
d.addCallback(lambda _: self.get_cmd_runner())
|
||||
d.addCallbacks(lambda _: log.info("Set up lbryum command runner"))
|
||||
d.addCallbacks(lambda _: check_locked())
|
||||
return d
|
||||
|
||||
def _stop(self):
|
||||
|
@ -1297,9 +1344,6 @@ class LBRYumWallet(Wallet):
|
|||
d.addCallback(lambda _: blockchain_caught_d)
|
||||
return d
|
||||
|
||||
def _get_cmd_runner(self):
|
||||
return Commands(self.config, self.wallet, self.network)
|
||||
|
||||
# run commands as a defer.succeed,
|
||||
# lbryum commands should be run this way , unless if the command
|
||||
# only makes a lbrum server query, use _run_cmd_as_defer_to_thread()
|
||||
|
@ -1517,6 +1561,24 @@ class LBRYumWallet(Wallet):
|
|||
def claim_renew(self, txid, nout):
|
||||
return self._run_cmd_as_defer_succeed('renewclaim', txid, nout)
|
||||
|
||||
def decrypt_wallet(self):
|
||||
if not self.wallet.use_encryption:
|
||||
return False
|
||||
if not self._cmd_runner:
|
||||
return False
|
||||
if self._cmd_runner.locked:
|
||||
return False
|
||||
self._cmd_runner.decrypt_wallet()
|
||||
return not self.wallet.use_encryption
|
||||
|
||||
def encrypt_wallet(self, new_password, update_keyring=False):
|
||||
if not self._cmd_runner:
|
||||
return False
|
||||
if self._cmd_runner.locked:
|
||||
return False
|
||||
self._cmd_runner.update_password(new_password, update_keyring)
|
||||
return not self.wallet.use_encryption
|
||||
|
||||
|
||||
class LBRYcrdAddressRequester(object):
|
||||
implements([IRequestCreator])
|
||||
|
|
|
@ -21,6 +21,8 @@ from lbryschema.error import URIParseError, DecodeError
|
|||
from lbryschema.validator import validate_claim_id
|
||||
from lbryschema.address import decode_address
|
||||
|
||||
from lbryum.errors import InvalidPassword
|
||||
|
||||
# TODO: importing this when internet is disabled raises a socket.gaierror
|
||||
from lbrynet.core.system_info import get_lbrynet_version
|
||||
from lbrynet import conf, analytics
|
||||
|
@ -165,7 +167,7 @@ class Daemon(AuthJSONRPCServer):
|
|||
"""
|
||||
|
||||
allowed_during_startup = [
|
||||
'daemon_stop', 'status', 'version',
|
||||
'daemon_stop', 'status', 'version', 'wallet_unlock'
|
||||
]
|
||||
|
||||
def __init__(self, analytics_manager):
|
||||
|
@ -541,6 +543,8 @@ class Daemon(AuthJSONRPCServer):
|
|||
elif self.wallet_type == LBRYUM_WALLET:
|
||||
log.info("Using lbryum wallet")
|
||||
config = {'auto_connect': True}
|
||||
if 'use_keyring' in conf.settings:
|
||||
config['use_keyring'] = conf.settings['use_keyring']
|
||||
if conf.settings['lbryum_wallet_dir']:
|
||||
config['lbryum_path'] = conf.settings['lbryum_wallet_dir']
|
||||
storage = SqliteStorage(self.db_dir)
|
||||
|
@ -971,16 +975,16 @@ class Daemon(AuthJSONRPCServer):
|
|||
Returns:
|
||||
(dict) lbrynet-daemon status
|
||||
{
|
||||
'lbry_id': lbry peer id, base58
|
||||
'installation_id': installation id, base58
|
||||
'is_running': bool
|
||||
'is_first_run': bool
|
||||
'lbry_id': lbry peer id, base58,
|
||||
'installation_id': installation id, base58,
|
||||
'is_running': bool,
|
||||
'is_first_run': bool,
|
||||
'startup_status': {
|
||||
'code': status code
|
||||
'code': status code,
|
||||
'message': status message
|
||||
},
|
||||
'connection_status': {
|
||||
'code': connection status code
|
||||
'code': connection status code,
|
||||
'message': connection status message
|
||||
},
|
||||
'blockchain_status': {
|
||||
|
@ -988,6 +992,7 @@ class Daemon(AuthJSONRPCServer):
|
|||
'blocks_behind': remote_height - local_height,
|
||||
'best_blockhash': block hash of most recent block,
|
||||
},
|
||||
'wallet_is_encrypted': bool,
|
||||
|
||||
If given the session status option:
|
||||
'session_status': {
|
||||
|
@ -1001,13 +1006,13 @@ class Daemon(AuthJSONRPCServer):
|
|||
'dht_status': {
|
||||
'kbps_received': current kbps receiving,
|
||||
'kbps_sent': current kdps being sent,
|
||||
'total_bytes_sent': total bytes sent
|
||||
'total_bytes_received': total bytes received
|
||||
'queries_received': number of queries received per second
|
||||
'queries_sent': number of queries sent per second
|
||||
'recent_contacts': count of recently contacted peers
|
||||
'total_bytes_sent': total bytes sent,
|
||||
'total_bytes_received': total bytes received,
|
||||
'queries_received': number of queries received per second,
|
||||
'queries_sent': number of queries sent per second,
|
||||
'recent_contacts': count of recently contacted peers,
|
||||
'unique_contacts': count of unique peers
|
||||
}
|
||||
},
|
||||
}
|
||||
"""
|
||||
|
||||
|
@ -1016,6 +1021,8 @@ class Daemon(AuthJSONRPCServer):
|
|||
local_height = self.session.wallet.network.get_local_height() if has_wallet else 0
|
||||
remote_height = self.session.wallet.network.get_server_height() if has_wallet else 0
|
||||
best_hash = (yield self.session.wallet.get_best_blockhash()) if has_wallet else None
|
||||
wallet_is_encrypted = has_wallet and self.session.wallet.wallet and \
|
||||
self.session.wallet.wallet.use_encryption
|
||||
|
||||
response = {
|
||||
'lbry_id': base58.b58encode(self.node_id),
|
||||
|
@ -1034,6 +1041,7 @@ class Daemon(AuthJSONRPCServer):
|
|||
else ''
|
||||
),
|
||||
},
|
||||
'wallet_is_encrypted': wallet_is_encrypted,
|
||||
'blocks_behind': remote_height - local_height, # deprecated. remove from UI, then here
|
||||
'blockchain_status': {
|
||||
'blocks': local_height,
|
||||
|
@ -1238,6 +1246,68 @@ class Daemon(AuthJSONRPCServer):
|
|||
return self._render_response(float(
|
||||
self.session.wallet.get_address_balance(address, include_unconfirmed)))
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def jsonrpc_wallet_unlock(self, password):
|
||||
"""
|
||||
Unlock an encrypted wallet
|
||||
|
||||
Usage:
|
||||
wallet_unlock (<password>)
|
||||
|
||||
Returns:
|
||||
(bool) true if wallet is unlocked, otherwise false
|
||||
"""
|
||||
|
||||
cmd_runner = self.session.wallet.get_cmd_runner()
|
||||
if cmd_runner is not None and cmd_runner.locked:
|
||||
result = True
|
||||
elif self.session.wallet.wallet_pw_d is not None:
|
||||
d = self.session.wallet.wallet_pw_d
|
||||
if not d.called:
|
||||
d.addCallback(lambda _: not self.session.wallet._cmd_runner.locked)
|
||||
self.session.wallet.wallet_pw_d.callback(password)
|
||||
try:
|
||||
result = yield d
|
||||
except InvalidPassword:
|
||||
result = False
|
||||
else:
|
||||
result = self.session.wallet._cmd_runner.locked
|
||||
response = yield self._render_response(result)
|
||||
defer.returnValue(response)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def jsonrpc_wallet_decrypt(self):
|
||||
"""
|
||||
Decrypt an encrypted wallet, this will remove the wallet password
|
||||
|
||||
Usage:
|
||||
wallet_decrypt
|
||||
|
||||
Returns:
|
||||
(bool) true if wallet is decrypted, otherwise false
|
||||
"""
|
||||
|
||||
result = self.session.wallet.decrypt_wallet()
|
||||
response = yield self._render_response(result)
|
||||
defer.returnValue(response)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def jsonrpc_wallet_encrypt(self, new_password):
|
||||
"""
|
||||
Encrypt a wallet with a password, if the wallet is already encrypted this will update
|
||||
the password
|
||||
|
||||
Usage:
|
||||
wallet_encrypt (<new_password>)
|
||||
|
||||
Returns:
|
||||
(bool) true if wallet is decrypted, otherwise false
|
||||
"""
|
||||
|
||||
self.session.wallet.encrypt_wallet(new_password)
|
||||
response = yield self._render_response(self.session.wallet.wallet.use_encryption)
|
||||
defer.returnValue(response)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def jsonrpc_daemon_stop(self):
|
||||
"""
|
||||
|
|
|
@ -1,10 +1,17 @@
|
|||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
import lbryum.wallet
|
||||
|
||||
from decimal import Decimal
|
||||
from collections import defaultdict
|
||||
from twisted.trial import unittest
|
||||
from twisted.internet import threads, defer
|
||||
|
||||
from lbrynet.core.Error import InsufficientFundsError
|
||||
from lbrynet.core.Wallet import Wallet, ReservedPoints, InMemoryStorage
|
||||
from lbrynet.core.Wallet import Wallet, LBRYumWallet, ReservedPoints, InMemoryStorage
|
||||
from lbryum.commands import Commands
|
||||
|
||||
|
||||
test_metadata = {
|
||||
'license': 'NASA',
|
||||
|
@ -44,8 +51,36 @@ class MocLbryumWallet(Wallet):
|
|||
return defer.succeed(True)
|
||||
|
||||
|
||||
class MocEncryptedWallet(LBRYumWallet):
|
||||
def __init__(self):
|
||||
LBRYumWallet.__init__(self, InMemoryStorage())
|
||||
self.wallet_balance = Decimal(10.0)
|
||||
self.total_reserved_points = Decimal(0.0)
|
||||
self.queued_payments = defaultdict(Decimal)
|
||||
|
||||
class WalletTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
wallet = MocEncryptedWallet()
|
||||
seed_text = "travel nowhere air position hill peace suffer parent beautiful rise " \
|
||||
"blood power home crumble teach"
|
||||
password = "secret"
|
||||
|
||||
user_dir = tempfile.mkdtemp()
|
||||
path = os.path.join(user_dir, "somewallet")
|
||||
storage = lbryum.wallet.WalletStorage(path)
|
||||
wallet.wallet = lbryum.wallet.NewWallet(storage)
|
||||
wallet.wallet.add_seed(seed_text, password)
|
||||
wallet.wallet.create_master_keys(password)
|
||||
wallet.wallet.create_main_account()
|
||||
|
||||
self.wallet_path = path
|
||||
self.enc_wallet = wallet
|
||||
self.enc_wallet_password = password
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(os.path.dirname(self.wallet_path))
|
||||
|
||||
def test_failed_send_name_claim(self):
|
||||
def not_enough_funds_send_name_claim(self, name, val, amount):
|
||||
claim_out = {'success':False, 'reason':'Not enough funds'}
|
||||
|
@ -198,3 +233,29 @@ class WalletTest(unittest.TestCase):
|
|||
'test', "f43dc06256a69988bdbea09a58c80493ba15dcfa", 4))
|
||||
self.assertFailure(d, InsufficientFundsError)
|
||||
return d
|
||||
|
||||
def test_unlock_wallet(self):
|
||||
wallet = self.enc_wallet
|
||||
wallet._cmd_runner = Commands(
|
||||
wallet.config, wallet.wallet, wallet.network, None, self.enc_wallet_password)
|
||||
cmd_runner = wallet.get_cmd_runner()
|
||||
cmd_runner.unlock_wallet(self.enc_wallet_password)
|
||||
self.assertIsNone(cmd_runner.new_password)
|
||||
self.assertEqual(cmd_runner._password, self.enc_wallet_password)
|
||||
|
||||
def test_encrypt_decrypt_wallet(self):
|
||||
wallet = self.enc_wallet
|
||||
wallet._cmd_runner = Commands(
|
||||
wallet.config, wallet.wallet, wallet.network, None, self.enc_wallet_password)
|
||||
wallet.encrypt_wallet("secret2", False)
|
||||
wallet.decrypt_wallet()
|
||||
|
||||
def test_update_password_keyring_off(self):
|
||||
wallet = self.enc_wallet
|
||||
wallet.config.use_keyring = False
|
||||
wallet._cmd_runner = Commands(
|
||||
wallet.config, wallet.wallet, wallet.network, None, self.enc_wallet_password)
|
||||
|
||||
# no keyring available, so ValueError is expected
|
||||
with self.assertRaises(ValueError):
|
||||
wallet.encrypt_wallet("secret2", True)
|
||||
|
|
|
@ -14,6 +14,7 @@ gmpy==1.17
|
|||
jsonrpc==1.2
|
||||
jsonrpclib==0.1.7
|
||||
jsonschema==2.6.0
|
||||
keyring==10.4.0
|
||||
git+https://github.com/lbryio/lbryum.git@v3.2.0rc7#egg=lbryum
|
||||
git+https://github.com/lbryio/lbryschema.git@v0.0.15rc2#egg=lbryschema
|
||||
miniupnpc==1.9
|
||||
|
@ -30,4 +31,4 @@ six>=1.9.0
|
|||
slowaes==0.1a1
|
||||
txJSON-RPC==0.5
|
||||
wsgiref==0.1.2
|
||||
zope.interface==4.3.3
|
||||
zope.interface==4.3.3
|
Loading…
Reference in a new issue