mirror of
https://github.com/Chia-Network/chia-blockchain.git
synced 2024-09-21 08:31:52 +03:00
a70082f29a
* Added 'service' as a Keychain ctor param. Removed 'testing' * Detect existing keys in the Mac Keychain * Fix to allow migration of keys on macOS * Added dump_keyring.py tool to show decrypted contents of keyring.yaml * Prompt to save passphrase to macOS keychain * Master passphrase retrieval/removal from the macOS Keychain. Fixed typos. * Warn if errSecInteractionNotAllowed is detected when accessing the macOS Keychain * Fixed file_keyring synchronization test failures on macOS. Fixed sporadic test failures on macOS when fsevents are delivered for the keyring after deletion. TempKeyring-based tests now patch supports_os_passphrase_storage() to return False. * TempKeyring mocks-out legacy_keyring setup to allow tests to succeed on macOS (which could find existing keys in the Keychain) * Fixed pylint error * Use with_name instead of with_stem (which is new to Python 3.9) * Fixed keychain tests that started prompting for the keyring passphrase. * Fixed LGTM issues * Re-added the cleaning up temp keychain statement. This is being removed in a separate PR. * Linter fixes * Fixed keyring assignment on macOS when passphrase support is disabled. * Include 'can_save_passphrase' flag in keyring_status response * More linter fixes * Fixed determination of the user_passphrase_is_set flag. This was returning true for a newly created keyring without any keys (or passphrase set) * Removed the tidy_passphrase function per feedback * Added some comments based on feedback * Update chia/cmds/passphrase_funcs.py Co-authored-by: Adam Kelly <338792+aqk@users.noreply.github.com> Co-authored-by: Adam Kelly <338792+aqk@users.noreply.github.com>
188 lines
7.4 KiB
Python
188 lines
7.4 KiB
Python
import logging
|
|
|
|
from blspy import PrivateKey
|
|
from chia.cmds.init_funcs import check_keys
|
|
from chia.util.keychain import Keychain
|
|
from pathlib import Path
|
|
from typing import Any, Dict, List, Optional, cast
|
|
|
|
# Commands that are handled by the KeychainServer
|
|
keychain_commands = [
|
|
"add_private_key",
|
|
"check_keys",
|
|
"delete_all_keys",
|
|
"delete_key_by_fingerprint",
|
|
"get_all_private_keys",
|
|
"get_first_private_key",
|
|
"get_key_for_fingerprint",
|
|
]
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
KEYCHAIN_ERR_KEYERROR = "key error"
|
|
KEYCHAIN_ERR_LOCKED = "keyring is locked"
|
|
KEYCHAIN_ERR_NO_KEYS = "no keys present"
|
|
KEYCHAIN_ERR_MALFORMED_REQUEST = "malformed request"
|
|
|
|
|
|
class KeychainServer:
|
|
"""
|
|
Implements a remote keychain service for clients to perform key operations on
|
|
"""
|
|
|
|
def __init__(self):
|
|
self._default_keychain = Keychain()
|
|
self._alt_keychains = {}
|
|
|
|
def get_keychain_for_request(self, request: Dict[str, Any]):
|
|
"""
|
|
Keychain instances can have user and service strings associated with them.
|
|
The keychain backends ultimately point to the same data stores, but the user
|
|
and service strings are used to partition those data stores. We attempt to
|
|
maintain a mapping of user/service pairs to their corresponding Keychain.
|
|
"""
|
|
keychain = None
|
|
user = request.get("kc_user", self._default_keychain.user)
|
|
service = request.get("kc_service", self._default_keychain.service)
|
|
if user == self._default_keychain.user and service == self._default_keychain.service:
|
|
keychain = self._default_keychain
|
|
else:
|
|
key = (user or "unnamed") + (service or "")
|
|
if key in self._alt_keychains:
|
|
keychain = self._alt_keychains[key]
|
|
else:
|
|
keychain = Keychain(user=user, service=service)
|
|
self._alt_keychains[key] = keychain
|
|
return keychain
|
|
|
|
async def handle_command(self, command, data) -> Dict[str, Any]:
|
|
if command == "add_private_key":
|
|
return await self.add_private_key(cast(Dict[str, Any], data))
|
|
elif command == "check_keys":
|
|
return await self.check_keys(cast(Dict[str, Any], data))
|
|
elif command == "delete_all_keys":
|
|
return await self.delete_all_keys(cast(Dict[str, Any], data))
|
|
elif command == "delete_key_by_fingerprint":
|
|
return await self.delete_key_by_fingerprint(cast(Dict[str, Any], data))
|
|
elif command == "get_all_private_keys":
|
|
return await self.get_all_private_keys(cast(Dict[str, Any], data))
|
|
elif command == "get_first_private_key":
|
|
return await self.get_first_private_key(cast(Dict[str, Any], data))
|
|
elif command == "get_key_for_fingerprint":
|
|
return await self.get_key_for_fingerprint(cast(Dict[str, Any], data))
|
|
return {}
|
|
|
|
async def add_private_key(self, request: Dict[str, Any]) -> Dict[str, Any]:
|
|
if self.get_keychain_for_request(request).is_keyring_locked():
|
|
return {"success": False, "error": KEYCHAIN_ERR_LOCKED}
|
|
|
|
mnemonic = request.get("mnemonic", None)
|
|
passphrase = request.get("passphrase", None)
|
|
if mnemonic is None or passphrase is None:
|
|
return {
|
|
"success": False,
|
|
"error": KEYCHAIN_ERR_MALFORMED_REQUEST,
|
|
"error_details": {"message": "missing mnemonic and/or passphrase"},
|
|
}
|
|
|
|
try:
|
|
self.get_keychain_for_request(request).add_private_key(mnemonic, passphrase)
|
|
except KeyError as e:
|
|
return {
|
|
"success": False,
|
|
"error": KEYCHAIN_ERR_KEYERROR,
|
|
"error_details": {"message": f"The word '{e.args[0]}' is incorrect.'", "word": e.args[0]},
|
|
}
|
|
|
|
return {"success": True}
|
|
|
|
async def check_keys(self, request: Dict[str, Any]) -> Dict[str, Any]:
|
|
if self.get_keychain_for_request(request).is_keyring_locked():
|
|
return {"success": False, "error": KEYCHAIN_ERR_LOCKED}
|
|
|
|
root_path = request.get("root_path", None)
|
|
if root_path is None:
|
|
return {
|
|
"success": False,
|
|
"error": KEYCHAIN_ERR_MALFORMED_REQUEST,
|
|
"error_details": {"message": "missing root_path"},
|
|
}
|
|
|
|
check_keys(Path(root_path))
|
|
|
|
return {"success": True}
|
|
|
|
async def delete_all_keys(self, request: Dict[str, Any]) -> Dict[str, Any]:
|
|
if self.get_keychain_for_request(request).is_keyring_locked():
|
|
return {"success": False, "error": KEYCHAIN_ERR_LOCKED}
|
|
|
|
self.get_keychain_for_request(request).delete_all_keys()
|
|
|
|
return {"success": True}
|
|
|
|
async def delete_key_by_fingerprint(self, request: Dict[str, Any]) -> Dict[str, Any]:
|
|
if self.get_keychain_for_request(request).is_keyring_locked():
|
|
return {"success": False, "error": KEYCHAIN_ERR_LOCKED}
|
|
|
|
fingerprint = request.get("fingerprint", None)
|
|
if fingerprint is None:
|
|
return {
|
|
"success": False,
|
|
"error": KEYCHAIN_ERR_MALFORMED_REQUEST,
|
|
"error_details": {"message": "missing fingerprint"},
|
|
}
|
|
|
|
self.get_keychain_for_request(request).delete_key_by_fingerprint(fingerprint)
|
|
|
|
return {"success": True}
|
|
|
|
async def get_all_private_keys(self, request: Dict[str, Any]) -> Dict[str, Any]:
|
|
all_keys: List[Dict[str, Any]] = []
|
|
if self.get_keychain_for_request(request).is_keyring_locked():
|
|
return {"success": False, "error": KEYCHAIN_ERR_LOCKED}
|
|
|
|
private_keys = self.get_keychain_for_request(request).get_all_private_keys()
|
|
for sk, entropy in private_keys:
|
|
all_keys.append({"pk": bytes(sk.get_g1()).hex(), "entropy": entropy.hex()})
|
|
|
|
return {"success": True, "private_keys": all_keys}
|
|
|
|
async def get_first_private_key(self, request: Dict[str, Any]) -> Dict[str, Any]:
|
|
key: Dict[str, Any] = {}
|
|
if self.get_keychain_for_request(request).is_keyring_locked():
|
|
return {"success": False, "error": KEYCHAIN_ERR_LOCKED}
|
|
|
|
sk_ent = self.get_keychain_for_request(request).get_first_private_key()
|
|
if sk_ent is None:
|
|
return {"success": False, "error": KEYCHAIN_ERR_NO_KEYS}
|
|
|
|
pk_str = bytes(sk_ent[0].get_g1()).hex()
|
|
ent_str = sk_ent[1].hex()
|
|
key = {"pk": pk_str, "entropy": ent_str}
|
|
|
|
return {"success": True, "private_key": key}
|
|
|
|
async def get_key_for_fingerprint(self, request: Dict[str, Any]) -> Dict[str, Any]:
|
|
if self.get_keychain_for_request(request).is_keyring_locked():
|
|
return {"success": False, "error": KEYCHAIN_ERR_LOCKED}
|
|
|
|
private_keys = self.get_keychain_for_request(request).get_all_private_keys()
|
|
if len(private_keys) == 0:
|
|
return {"success": False, "error": KEYCHAIN_ERR_NO_KEYS}
|
|
|
|
fingerprint = request.get("fingerprint", None)
|
|
private_key: Optional[PrivateKey] = None
|
|
entropy: Optional[bytes] = None
|
|
if fingerprint is not None:
|
|
for sk, entropy in private_keys:
|
|
if sk.get_g1().get_fingerprint() == fingerprint:
|
|
private_key = sk
|
|
break
|
|
else:
|
|
private_key, entropy = private_keys[0]
|
|
|
|
if not private_key or not entropy:
|
|
return {"success": False, "error": KEYCHAIN_ERR_NO_KEYS}
|
|
else:
|
|
return {"success": True, "pk": bytes(private_key.get_g1()).hex(), "entropy": entropy.hex()}
|