From b916275540451ff38b1c2456c294924eea306ac2 Mon Sep 17 00:00:00 2001 From: dustinface <35775977+xdustinface@users.noreply.github.com> Date: Sun, 4 Dec 2022 06:31:51 +0100 Subject: [PATCH] tools: Implement and test `tools/legacy_keyring.py` (#13947) * Add `-l` option to `install.sh` and use it for linux tests on CI * Implement and test `tools/legacy_keyring.py` * Update install.sh Co-authored-by: Jeff Co-authored-by: Jeff --- .github/actions/install/action.yml | 6 +- .github/workflows/benchmarks.yml | 1 + .github/workflows/test-single.yml | 1 + install.sh | 7 +- setup.py | 5 + tests/build-job-matrix.py | 1 + tests/tools/config.py | 3 + tests/tools/test_legacy_keyring.py | 82 ++++++++++++++++ tools/legacy_keyring.py | 153 +++++++++++++++++++++++++++++ 9 files changed, 256 insertions(+), 3 deletions(-) create mode 100644 tests/tools/test_legacy_keyring.py create mode 100644 tools/legacy_keyring.py diff --git a/.github/actions/install/action.yml b/.github/actions/install/action.yml index 2bd56b190ec49..17ed8d273d795 100644 --- a/.github/actions/install/action.yml +++ b/.github/actions/install/action.yml @@ -11,6 +11,10 @@ inputs: description: "Install development dependencies." required: false default: "" + legacy_keyring: + description: "Install legacy keyring dependencies." + required: false + default: "" automated: description: "Automated install, no questions." required: false @@ -29,7 +33,7 @@ runs: env: INSTALL_PYTHON_VERSION: ${{ inputs.python-version }} run: | - ${{ inputs.command-prefix }} sh install.sh ${{ inputs.development && '-d' || '' }} ${{ inputs.automated == 'true' && '-a' || '' }} + ${{ inputs.command-prefix }} sh install.sh ${{ inputs.development && '-d' || '' }} ${{ inputs.legacy_keyring && '-l' || '' }} ${{ inputs.automated == 'true' && '-a' || '' }} - name: Run install script (Windows) if: runner.os == 'windows' diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 62527fa8823f7..4c35dec5ac9fe 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -71,6 +71,7 @@ jobs: with: python-version: ${{ matrix.python-version }} development: true + legacy_keyring: true - uses: chia-network/actions/activate-venv@main diff --git a/.github/workflows/test-single.yml b/.github/workflows/test-single.yml index 37e35d04f8efa..15af4174b5285 100644 --- a/.github/workflows/test-single.yml +++ b/.github/workflows/test-single.yml @@ -197,6 +197,7 @@ jobs: with: python-version: ${{ matrix.python.install_sh }} development: true + legacy_keyring: ${{ matrix.configuration.legacy_keyring_required }} - uses: chia-network/actions/activate-venv@main diff --git a/install.sh b/install.sh index 4c1909d51f888..cae628658a012 100755 --- a/install.sh +++ b/install.sh @@ -3,10 +3,11 @@ set -o errexit USAGE_TEXT="\ -Usage: $0 [-adsph] +Usage: $0 [-adlsph] -a automated install, no questions -d install development dependencies + -l install legacy keyring dependencies (linux only) -s skip python package installation and just do pip install -p additional plotters installation -h display this help and exit @@ -21,7 +22,7 @@ EXTRAS= SKIP_PACKAGE_INSTALL= PLOTTER_INSTALL= -while getopts adsph flag +while getopts adlsph flag do case "${flag}" in # automated @@ -31,6 +32,8 @@ do # simple install s) SKIP_PACKAGE_INSTALL=1;; p) PLOTTER_INSTALL=1;; + # legacy keyring + l) EXTRAS=${EXTRAS}legacy_keyring,;; h) usage; exit 0;; *) echo; usage; exit 1;; esac diff --git a/setup.py b/setup.py index 311e090d87993..fb5800b3f48ce 100644 --- a/setup.py +++ b/setup.py @@ -67,6 +67,10 @@ dev_dependencies = [ "types-setuptools", ] +legacy_keyring_dependencies = [ + "keyrings.cryptfile==1.3.9", +] + kwargs = dict( name="chia-blockchain", author="Mariano Sorgente", @@ -80,6 +84,7 @@ kwargs = dict( extras_require=dict( dev=dev_dependencies, upnp=upnp_dependencies, + legacy_keyring=legacy_keyring_dependencies, ), packages=[ "build_scripts", diff --git a/tests/build-job-matrix.py b/tests/build-job-matrix.py index 01b5c608cfbd7..a2d25f5c22861 100644 --- a/tests/build-job-matrix.py +++ b/tests/build-job-matrix.py @@ -116,6 +116,7 @@ for path in test_paths: "install_timelord": conf["install_timelord"], "test_files": paths_for_cli, "name": ".".join(path.relative_to(root_path).with_suffix("").parts), + "legacy_keyring_required": conf.get("legacy_keyring_required", False), } for_matrix = dict(sorted(for_matrix.items())) configuration.append(for_matrix) diff --git a/tests/tools/config.py b/tests/tools/config.py index 562db3171ae44..785fcf2b606cd 100644 --- a/tests/tools/config.py +++ b/tests/tools/config.py @@ -1,3 +1,6 @@ from __future__ import annotations +import sys + parallel = True +legacy_keyring_required = sys.platform == "linux" diff --git a/tests/tools/test_legacy_keyring.py b/tests/tools/test_legacy_keyring.py new file mode 100644 index 0000000000000..65ea9d12faafb --- /dev/null +++ b/tests/tools/test_legacy_keyring.py @@ -0,0 +1,82 @@ +from __future__ import annotations + +import sys +from pathlib import Path + +import pytest +from click.testing import CliRunner, Result + +try: + from keyrings.cryptfile.cryptfile import CryptFileKeyring +except ImportError: + if sys.platform == "linux": + raise + +from tools.legacy_keyring import create_legacy_keyring, generate_and_add, get_keys, legacy_keyring + + +def show() -> Result: + return CliRunner().invoke(legacy_keyring, ["show"]) + + +def clear(input_str: str) -> Result: + return CliRunner().invoke(legacy_keyring, ["clear"], input=f"{input_str}\n") + + +@pytest.mark.skipif(sys.platform == "win32" or sys.platform == "darwin", reason="Tests the linux legacy keyring format") +def test_legacy_keyring_format(tmp_dir: Path) -> None: + keyring = CryptFileKeyring() + keyring.keyring_key = "your keyring password" + # Create the legacy keyring file with the old format + keyring.file_path = tmp_dir / "keyring" + keyring.filename = keyring.file_path.name + keyring_data = """ + [chia_2Duser_2Dchia_2D1_2E8] + wallet_2duser_2dchia_2d1_2e8_2d0 = + eyJzYWx0IjogIi9NY3J3UG9iQjdiclpQMGRHclZiU1E9PSIsICJkYXRhIjogIjBnMEROUzRDSGdJ + NU4yVEFYUVVhaExFY2RzN0NFR05rNnpKSmNLcWY5VmdOb2h6SkdxcUlOZzNKaTBEa3NIOGh3aHlM + cG1GeFZVYWRcbmRtMTVWMDlsU3I1b3dNZDZHY3JGQTJHckZtZGszUmFmY0ZicmhlMmlRMjMzRW1P + c28zQUxNbG5CcGtWTlR0cHZYYjlzbEp4VE5yVVVcbm8xUE0wNytTa1lJTHVzcmlNUStkUjBIQkxZ + WXF3VjBUVndETHVKZmdtNWdyd1hrUkdkUjdvU0VyVTJUcnRnPT0iLCAibWFjIjogInA4MWJFTXhJ + ay83bm1iMDMxR0NpZnc9PSIsICJub25jZSI6ICJzcUhoTUhOMkZQeTQxR3U4em40MXhBPT0ifQ== + wallet_2duser_2dchia_2d1_2e8_2d1 = + eyJzYWx0IjogIjNhWkFCQXBCcXUxdzI5WHpJcXBzS3c9PSIsICJkYXRhIjogImZwU05ZYk5WMmJM + Vms5MjB6cGYzdzYrK2ZMc2w4b3Y4OU9uTWdHNlo4OXhzenRoc0tFZjdieHVKVGRyT3JmYmtBUmgv + TzhzY3R1R2ZcblR1REVIOHJHNVA3RGpOWWQ3dFhxd2xabkg1VTVnV2VCNzZPaXdmVDQxQytxWlVX + RXQ5L1dnMTQybHdqMy8vR2pJZ0w2d2Q0QXQyWjBcbmtQQVNOMnVnVmZpa0RiZGFaN21oeFRxNnRK + TEszQWtLU3VPVmJyWEplbjZ2OGhXcGNMVU1HN3RIZENWNU5nPT0iLCAibWFjIjogIitPS3h1ZjZQ + RzArdTA2Z2Qzb2dSNGc9PSIsICJub25jZSI6ICIxdWR2N1JIajhWaER2UWpVSjRJLzZnPT0ifQ== + """ + with open(str(keyring.file_path), "w") as keyring_file: + keyring_file.write(keyring_data) + # Make sure the loaded keys match the file content + keys = get_keys(keyring) + assert len(keys) == 2 + assert keys[0].fingerprint == 1925978301 + assert keys[1].fingerprint == 2990446712 + + +def test_legacy_keyring_cli() -> None: + keyring = create_legacy_keyring() + result = show() + assert result.exit_code == 1 + assert "No keys found in the legacy keyring." in result.output + keys = [] + for i in range(5): + keys.append(generate_and_add(keyring)) + result = show() + assert result.exit_code == 0 + for key in keys: + assert key.mnemonic_str() in result.output + + # Should abort if the prompt gets a `n` + result = clear("n") + assert result.exit_code == 1 + assert "Aborted" in result.output + + # And succeed if the prompt gets a `y` + result = clear("y") + assert result.exit_code == 0 + for key in keys: + assert key.mnemonic_str() in result.output + assert f"{len(keys)} keys removed" in result.output diff --git a/tools/legacy_keyring.py b/tools/legacy_keyring.py new file mode 100644 index 0000000000000..b4c258658ccaa --- /dev/null +++ b/tools/legacy_keyring.py @@ -0,0 +1,153 @@ +""" +Provides a helper to access the legacy keyring which was supported up to version 1.6.1 of chia-blockchain. To use this +helper it's required to install the `legacy_keyring` extra dependency which can be done via the install-option `-l`. +""" + +from __future__ import annotations + +import sys +from typing import Callable, List, Union, cast + +import click +from blspy import G1Element +from keyring.backends.macOS import Keyring as MacKeyring +from keyring.backends.Windows import WinVaultKeyring as WinKeyring + +try: + from keyrings.cryptfile.cryptfile import CryptFileKeyring +except ImportError: + if sys.platform == "linux": + sys.exit("Use `install.sh -l` to install the legacy_keyring dependency.") + CryptFileKeyring = None + + +from chia.util.errors import KeychainUserNotFound +from chia.util.keychain import KeyData, KeyDataSecrets, get_private_key_user +from chia.util.misc import prompt_yes_no + +LegacyKeyring = Union[MacKeyring, WinKeyring, CryptFileKeyring] + + +CURRENT_KEY_VERSION = "1.8" +DEFAULT_USER = f"user-chia-{CURRENT_KEY_VERSION}" # e.g. user-chia-1.8 +DEFAULT_SERVICE = f"chia-{DEFAULT_USER}" # e.g. chia-user-chia-1.8 +MAX_KEYS = 100 + + +# casting to compensate for a combination of mypy and keyring issues +# https://github.com/python/mypy/issues/9025 +# https://github.com/jaraco/keyring/issues/437 +def create_legacy_keyring() -> LegacyKeyring: + if sys.platform == "darwin": + return cast(Callable[[], LegacyKeyring], MacKeyring)() + elif sys.platform == "win32" or sys.platform == "cygwin": + return cast(Callable[[], LegacyKeyring], WinKeyring)() + elif sys.platform == "linux": + keyring: CryptFileKeyring = CryptFileKeyring() + keyring.keyring_key = "your keyring password" + return keyring + raise click.ClickException(f"platform '{sys.platform}' not supported.") + + +def generate_and_add(keyring: LegacyKeyring) -> KeyData: + key = KeyData.generate() + index = 0 + while True: + try: + get_key_data(keyring, index) + index += 1 + except KeychainUserNotFound: + keyring.set_password( + DEFAULT_SERVICE, + get_private_key_user(DEFAULT_USER, index), + bytes(key.public_key).hex() + key.entropy.hex(), + ) + return key + + +def get_key_data(keyring: LegacyKeyring, index: int) -> KeyData: + user = get_private_key_user(DEFAULT_USER, index) + read_str = keyring.get_password(DEFAULT_SERVICE, user) + if read_str is None or len(read_str) == 0: + raise KeychainUserNotFound(DEFAULT_SERVICE, user) + str_bytes = bytes.fromhex(read_str) + + public_key = G1Element.from_bytes(str_bytes[: G1Element.SIZE]) + fingerprint = public_key.get_fingerprint() + entropy = str_bytes[G1Element.SIZE : G1Element.SIZE + 32] + + return KeyData( + fingerprint=fingerprint, + public_key=public_key, + label=None, + secrets=KeyDataSecrets.from_entropy(entropy), + ) + + +def get_keys(keyring: LegacyKeyring) -> List[KeyData]: + keys: List[KeyData] = [] + for index in range(MAX_KEYS + 1): + try: + keys.append(get_key_data(keyring, index)) + except KeychainUserNotFound: + pass + return keys + + +def print_key(key: KeyData) -> None: + print(f"fingerprint: {key.fingerprint}, mnemonic: {key.mnemonic_str()}") + + +def print_keys(keyring: LegacyKeyring) -> None: + keys = get_keys(keyring) + + if len(keys) == 0: + raise click.ClickException("No keys found in the legacy keyring.") + + for key in keys: + print_key(key) + + +def remove_keys(keyring: LegacyKeyring) -> None: + removed = 0 + for index in range(MAX_KEYS + 1): + try: + keyring.delete_password(DEFAULT_SERVICE, get_private_key_user(DEFAULT_USER, index)) + removed += 1 + except Exception: + pass + + print(f"{removed} key{'s' if removed != 1 else ''} removed.") + + +@click.group(help="Manage the keys in the legacy keyring.") +def legacy_keyring() -> None: + pass + + +@legacy_keyring.command(help="Generate and add a random key (for testing)", hidden=True) +def generate() -> None: + keyring = create_legacy_keyring() + key = generate_and_add(keyring) + print_key(key) + + +@legacy_keyring.command(help="Show all available keys") +def show() -> None: + print_keys(create_legacy_keyring()) + + +@legacy_keyring.command(help="Remove all keys") +def clear() -> None: + keyring = create_legacy_keyring() + + print_keys(keyring) + + if not prompt_yes_no("\nDo you really want to remove all the keys from the legacy keyring? This can't be undone."): + raise click.ClickException("Aborted!") + + remove_keys(keyring) + + +if __name__ == "__main__": + legacy_keyring()