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 <paninaro@gmail.com>

Co-authored-by: Jeff <paninaro@gmail.com>
This commit is contained in:
dustinface 2022-12-04 06:31:51 +01:00 committed by GitHub
parent 306b318bcd
commit b916275540
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 256 additions and 3 deletions

View File

@ -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'

View File

@ -71,6 +71,7 @@ jobs:
with:
python-version: ${{ matrix.python-version }}
development: true
legacy_keyring: true
- uses: chia-network/actions/activate-venv@main

View File

@ -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

View File

@ -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

View File

@ -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",

View File

@ -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)

View File

@ -1,3 +1,6 @@
from __future__ import annotations
import sys
parallel = True
legacy_keyring_required = sys.platform == "linux"

View File

@ -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

153
tools/legacy_keyring.py Normal file
View File

@ -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()