chia-blockchain/chia/rpc/farmer_rpc_api.py
Jeff Cruikshank 325d7f8245
Keyring passphrase protection (#7249)
* Moved keyring handling into a KeyringWrapper class

* Update click to 8.0.x for prompt_required support

* Renamed KeyringWrapper to _KeyringWrapper

* Expose password management options on Linux

* CLI support for setting/removing a password

* Global option for specifying the master password

* Cache the password instead of setting on the context

* Password bootstrapping during chia init

* Tidying up _KeyringWraper's interface

* Initial pass migrating the legacy keyring contents

* Encryption/decryption of keyring.yaml contents

* FileKeyring backend encrypts with ChaCha20Poly1305

* Tightened up keyring migration and initialization

* Fixed issues identified by linters

* Remove root_path from Keychain

* Prevent double-migration if setting master passwd

* KeyringWrapper tests are mostly complete

* FileKeyring will now honor the service param

* Tests for get/set/delete password

* Formatting/commenting updates

* Writer lock support with tests - WIP

* keyring.yaml is now watched for modifications

* Reader/Writer lock for get/delete password

* Fixed linter issues

* Reader lock tests

* Formatting update

* Hook up CHIA_ROOT support for KeychainWrapper

* Quick fix to address test failures

* Fixed failures when existing legacy keyring exists

* Fixed test failures caused by reusing the same temp dir

* keyring.yaml now lives in ~/.chia_keys by default. Can be overridden with CHIA_KEYS_ROOT or --keys-root-path

* Fixed migration failure when setting a password (not using the default)

* KeyringWrapper now uses supports_keyring_password to determine if a FileKeyring should be used. Patched tests to work regardless of whether supports_keyring_password return False

* The daemon now takes a --have-gui option that will prevent calling check_keys() during startup. If the keyring is locked, we want the GUI to prompt for the password.

* Added is_keyring_locked RPC call

* Added 'unlock_keyring' RPC command

* Added KeychainProxy and KeychainServer to handle RPC messages related to keyring operations. WalletNode no longer directly accesses the Keychain class.

* Turn on macOS support for testing keyring passwords

* Fixed get_key_for_fingerprint to use the ocal keychain if the platform doesn't need to remotely access the daemon's keychain.

Fixed key reconstruction when sent over RPC.

* Farmer now accesses the keychain over RPC

* Fixes for linter issues and some restructuring to support tests that use setup_nodes.py

* Couple of fixes to unblock the GUI from launching when a keyring password is set

* Added a keychain RPC call for add_private_key()

* Added remaining keychain proxy RPC calls for delete_key_by_fingerprint and delete_all_keys

* Check for None when inspecting request arguments

* Run check_keys after unlocking the keyring when the daemon is launched via GUI

* Added check_keys RPC method.
Fixed deserialization of key entropy in get_all_private_keys. This was preventing the GUI from being able to show key details.

* Added get_first_private_key to keychain_server/proxy.
create_plots now uses the keychain proxy when launched from the daemon.

* Added a comment about KeychainProxy in chia plots check

* Workaround import conflict  when importing from 'tests.*' due to fasteners name conflict

* Simulator now uses KeychainProxy if launched by the daemon.
KeychainServer/Proxy now takes keychain user/testing params for testing scenarios.

* Added "set_keyring_passphrase" RPC message

* Reworking KeychainProxy usage to handle local keychain tests and RPC keychain tests.

* Replace my prior usage of asyncio.run() with asyncio.get_event_loop().run_until_complete()

* Silencing file_keyring logging for the moment.

* Updated tests to use test keychains and appropriate BlockTools construction

BlockTools should now be created with create_block_tools(_async) to handle async scenarios.

Updated block_tools to be async compatible

Updated fasteners to fix installation of top-level 'tests' in site-packages

* Added 'remove_keyring_passphrase' RPC message to the daemon

Minor tweak to TempKeyring to default to some test params

* Fixed linter issues

* Remove flake8 ignore statement now that the fasteners module has been updated

* Some initial renaming changes: password -> passphrase

* Fixed wallet RPC issue where get_key_for_fingerprint wasn't awaited-upon.

Fixed legacy keyring initialization (for migration scenarios)

* Fixed improperly merged file

* Fixed linter issues.
More renaming.

* Updated spots that were still using an incorrect keychain call

* Renamed use_password_cache, obtain_current_password

* Renamed supports_keyring_password

* Renamed has_master_password

* Renamed has_cached_password, get_cached_master_password

* Linter fixes

* Renamed master_password_is_valid

* Renamed set_cached_master_password

* Renamed set_master_password

* Renamed remove_master_password

* Renamed has_cached_master_password

* Renaming in file_keyring and keyring_wrapper

Updated default keyring payload used for tests

* Renamed get_password
Other renaming updates

* Renamed set_password
Other renaming updates

* Renamed remaining password occurrences (where appropriate)

* password -> passphrase

* Added tests for setting an emoji and Japanese master passphrase

* Attempt to notify the daemon when a keyring passphrase is set/updated/removed

* Missed one password -> passphrase replacement.

* Fixed some file synchronization issues found when running tests on macOS

* Adjusted timeout values for test_writer_lock_reacquisition_failure for macOS.

* Removed logging statements previously added for debugging

* Prompt for keyring passphrase up-front when launching a service.

Changed --have-gui flag to --wait-for-unlock

* Updated set_keyring_passphrase RPC message to fix optional current_passphrase param when the keyring is using the default passphrase.

* Minor test cleanup to deduplicate some code.

* Fixed regression when setting a new master passphrase

* Minor refactoring and docs/commenting updates

* Renaming password -> passphrase went too far. Keyring backends use password terminology for compatibility with third party backends.

* Disabling macOS support (previously added for testing only)

* Disabling passphrase support in preparation for sending out the PR

* Fixed improper merge (vscode didn't save changes during rebase)

* Update chia/cmds/init_funcs.py

Co-authored-by: Adam Kelly <338792+aqk@users.noreply.github.com>

* skip_check_keys -> should_check_keys

* Shuffling some imports around to break cycles reported by LGTM

* Handle unlocking the daemon if it's already launched and waiting for unlock.

* Replaced uses_keychain_proxy decorator in farmer.py. Fixed async usage of get_reward_targets.

Linter/reformatting fixes

* Replaced uses_keychain_proxy decorator with a clearer method.

* Cleanup the temp keyring dir using shutil.rmtree()

* Restored self._root_path (had been changed to self.root_path)

* Minor cleanup

* ensure_keychain_proxy() now throws if connect_to_keychain_and_validate() fails

* Plot key resolution now yields a PlotKeys object which can be passed into create_plots.

De-indented test_invalid_icc_sub_slot_vdf to keep git blame tidy.

* Added 'keyring_status' daemon RPC message to support the GUI

* Minor changes relating to PR feedback

* Addressed more PR feedback (mostly type annotations)

* Commented-out macOS file keyring usage. This can be re-enabled for testing purposes.

* Addressed test failures that require multiple keyrings in the same process. Each TempKeyring will now set a custom KeyringWrapper instance.

* Fixed logic for communicating user_passphrase_is_set in the keyring_status RPC response.

* Updated type annotations and method signature for set_passphrase to expect a string instead of bytes.

* Fixed Wallet RPC tests

* Fixed full_node_store tests. BlockTools should be created using the create_block_tools(_async) function(s)

* Fixed test failures in test_pool_rpc

* Fixed test_daemon. After BlockTools.setup_plots is run, the config file needs to be re-read to refresh stale plot_directories.

* Suppressing LGTM false positives regarding passphrase leakage in CLI error output. Seems that LGTM sees MIN_PASSPHRASE_LEN as sensitive data.

* Second attempt at suppressing LGTM false positives

* Third attempt at addressing LGTM false positives

* Removed test_keyring_wrapper param from Keychain ctor. Test setup now sets the keyring_wrapper property directly.

* Reformatting

* More targeted update of the test config to refresh just the "plot_directories" value

* More LGTM suppressions

Co-authored-by: Adam Kelly <338792+aqk@users.noreply.github.com>
Co-authored-by: wjblanke <wjb98672@gmail.com>
2021-08-04 12:46:55 -07:00

134 lines
5.4 KiB
Python

from typing import Callable, Dict, List, Optional
from chia.farmer.farmer import Farmer
from chia.types.blockchain_format.sized_bytes import bytes32
from chia.util.byte_types import hexstr_to_bytes
from chia.util.ws_message import WsRpcMessage, create_payload_dict
class FarmerRpcApi:
def __init__(self, farmer: Farmer):
self.service = farmer
self.service_name = "chia_farmer"
def get_routes(self) -> Dict[str, Callable]:
return {
"/get_signage_point": self.get_signage_point,
"/get_signage_points": self.get_signage_points,
"/get_reward_targets": self.get_reward_targets,
"/set_reward_targets": self.set_reward_targets,
"/get_pool_state": self.get_pool_state,
"/set_payout_instructions": self.set_payout_instructions,
"/get_harvesters": self.get_harvesters,
"/get_pool_login_link": self.get_pool_login_link,
}
async def _state_changed(self, change: str, change_data: Dict) -> List[WsRpcMessage]:
if change == "new_signage_point":
sp_hash = change_data["sp_hash"]
data = await self.get_signage_point({"sp_hash": sp_hash.hex()})
return [
create_payload_dict(
"new_signage_point",
data,
self.service_name,
"wallet_ui",
)
]
elif change == "new_farming_info":
return [
create_payload_dict(
"new_farming_info",
change_data,
self.service_name,
"wallet_ui",
)
]
elif change == "new_plots":
return [
create_payload_dict(
"get_harvesters",
change_data,
self.service_name,
"wallet_ui",
)
]
return []
async def get_signage_point(self, request: Dict) -> Dict:
sp_hash = hexstr_to_bytes(request["sp_hash"])
for _, sps in self.service.sps.items():
for sp in sps:
if sp.challenge_chain_sp == sp_hash:
pospaces = self.service.proofs_of_space.get(sp.challenge_chain_sp, [])
return {
"signage_point": {
"challenge_hash": sp.challenge_hash,
"challenge_chain_sp": sp.challenge_chain_sp,
"reward_chain_sp": sp.reward_chain_sp,
"difficulty": sp.difficulty,
"sub_slot_iters": sp.sub_slot_iters,
"signage_point_index": sp.signage_point_index,
},
"proofs": pospaces,
}
raise ValueError(f"Signage point {sp_hash.hex()} not found")
async def get_signage_points(self, _: Dict) -> Dict:
result: List = []
for _, sps in self.service.sps.items():
for sp in sps:
pospaces = self.service.proofs_of_space.get(sp.challenge_chain_sp, [])
result.append(
{
"signage_point": {
"challenge_hash": sp.challenge_hash,
"challenge_chain_sp": sp.challenge_chain_sp,
"reward_chain_sp": sp.reward_chain_sp,
"difficulty": sp.difficulty,
"sub_slot_iters": sp.sub_slot_iters,
"signage_point_index": sp.signage_point_index,
},
"proofs": pospaces,
}
)
return {"signage_points": result}
async def get_reward_targets(self, request: Dict) -> Dict:
search_for_private_key = request["search_for_private_key"]
return await self.service.get_reward_targets(search_for_private_key)
async def set_reward_targets(self, request: Dict) -> Dict:
farmer_target, pool_target = None, None
if "farmer_target" in request:
farmer_target = request["farmer_target"]
if "pool_target" in request:
pool_target = request["pool_target"]
self.service.set_reward_targets(farmer_target, pool_target)
return {}
async def get_pool_state(self, _: Dict) -> Dict:
pools_list = []
for p2_singleton_puzzle_hash, pool_dict in self.service.pool_state.items():
pool_state = pool_dict.copy()
pool_state["p2_singleton_puzzle_hash"] = p2_singleton_puzzle_hash.hex()
pools_list.append(pool_state)
return {"pool_state": pools_list}
async def set_payout_instructions(self, request: Dict) -> Dict:
launcher_id: bytes32 = hexstr_to_bytes(request["launcher_id"])
await self.service.set_payout_instructions(launcher_id, request["payout_instructions"])
return {}
async def get_harvesters(self, _: Dict):
return await self.service.get_harvesters()
async def get_pool_login_link(self, request: Dict) -> Dict:
launcher_id: bytes32 = bytes32(hexstr_to_bytes(request["launcher_id"]))
login_link: Optional[str] = await self.service.generate_login_link(launcher_id)
if login_link is None:
raise ValueError(f"Failed to generate login link for {launcher_id.hex()}")
return {"login_link": login_link}