diff --git a/README.md b/README.md index 1f59c685..0363c042 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,29 @@ rm -rf trader Then continue above with "Run the script". +## Change the password of your key files + +> :warning: **Warning**
+> The code within this repository is provided without any warranties. It is important to note that the code has not been audited for potential security vulnerabilities. +> +> If you are updating the password for your key files, it is strongly advised to create a backup of the old configuration (located in the `./trader_runner` folder) before proceeding. This backup should be retained until you can verify that the changes are functioning as expected. For instance, run the service multiple times to ensure there are no issues with the new password before discarding the backup. + +If you have started you script specifying a password to protect your key files, you can change it by running the following command: + +```bash +cd trader; poetry run python ../scripts/change_keys_json_password.py ../.trader_runner --current_password --new_password ; cd .. +``` + +This will change the password in the following files: + +- `.trader_runner/keys.json` +- `.trader_runner/operator_keys.json` +- `.trader_runner/agent_pkey.txt` +- `.trader_runner/operator_pkey.txt` + +If your key files are not encrypted, you must not use the `--current-password` argument. If you want to remove the password protection of your key files, +you must not specify the `--new-password` argument. + ## Advice for Mac users In Docker Desktop make sure that in `Settings -> Advanced` the following boxes are ticked diff --git a/run_service.sh b/run_service.sh index 67ac57e9..5345c236 100755 --- a/run_service.sh +++ b/run_service.sh @@ -241,6 +241,92 @@ get_on_chain_service_state() { echo "$state" } +# Asks if user wishes to use password-protected key files +ask_confirm_password() { + echo "Use a password?" + echo "---------------" + echo "You can use a password to encrypt the generated key files. You will be asked for the password each time the script is run." + while true; do + read -p "Do you want to use a password? (yes/no): " use_password + case "$use_password" in + [Yy]|[Yy][Ee][Ss]) + echo "WARNING:" + echo " - Passwords are case-sensitive. Check your Caps Lock before continuing." + echo " - Passwords are not stored on disk." + echo " - If you lose your password, you will lose access to all assets associated to your operator or trader agent keys." + echo "" + while true; do + read -s -p "Enter your password: " password + echo "" + + read -s -p "Confirm your password: " confirm_password + echo "" + + if [ -z "$password" ]; then + echo "Password cannot be blank. Please try again." + elif [[ -n $(echo "-$password-" | awk '{ if(match($0, /[ \t]/)) print "contains_whitespace"; }') ]]; then + echo "Password cannot contain whitespace characters. Please try again." + elif [ ${#password} -lt 4 ]; then + echo "Password must be at least 4 characters long. Please try again." + elif [ "$password" = "$confirm_password" ]; then + use_password=true + password_argument="--password $password" + echo "Password confirmed. Please, store your pasword in a safe place." + read -n 1 -s -r -p "Press any key to continue..." + echo "" + echo "" + return 0 + else + echo "Passwords do not match. Please try again." + fi + done + ;; + [Nn]|[Nn][Oo]) + use_password=false + password_argument="" + echo "" + return 0 + ;; + * ) + echo "Please enter 'yes' or 'no'." + ;; + esac + done + echo "" + return 0 +} + +# Asks password if key files are password-protected +ask_password_if_needed() { + agent_pkey=$(get_private_key "$keys_json_path") + if [[ "$agent_pkey" = *crypto* ]]; then + echo "Enter your password" + echo "-------------------" + echo "Your key files are protected with a password." + read -s -p "Please, enter your password: " password + use_password=true + password_argument="--password $password" + echo "" + else + echo "Your key files are not protected with a password." + use_password=false + password_argument="" + fi + echo "" +} + +# Validates the provided password +validate_password() { + local is_password_valid_1=$(poetry run $PYTHON_CMD ../scripts/is_keys_json_password_valid.py ../$keys_json_path $password_argument) + local is_password_valid_2=$(poetry run $PYTHON_CMD ../scripts/is_keys_json_password_valid.py ../$operator_keys_file $password_argument) + + if [ "$is_password_valid_1" != "True" ] || [ "$is_password_valid_2" != "True" ]; then + echo "Could not decrypt key files. Please verify if your key files are password-protected, and if the provided password is correct (passwords are case-sensitive)." + echo "Terminating the script." + exit 1 + fi +} + # Function to retrieve the multisig address of a service get_multisig_address() { local service_id="$1" @@ -252,14 +338,16 @@ get_multisig_address() { # stake or unstake a service perform_staking_ops() { local unstake="$1" - poetry run python "../scripts/staking.py" "$service_id" "$CUSTOM_SERVICE_REGISTRY_ADDRESS" "$CUSTOM_STAKING_ADDRESS" "../$operator_pkey_path" "$rpc" "$unstake" + poetry run python "../scripts/staking.py" "$service_id" "$CUSTOM_SERVICE_REGISTRY_ADDRESS" "$CUSTOM_STAKING_ADDRESS" "../$operator_pkey_path" "$rpc" "$unstake" $password_argument echo "" } # Prompt user for staking preference prompt_use_staking() { while true; do - read -p "Do you want to use staking in this service? (yes/no): " use_staking + echo "Use staking?" + echo "------------" + read -p "Do you want to use this service in a staking program? (yes/no): " use_staking case "$use_staking" in [Yy]|[Yy][Ee][Ss]) @@ -275,6 +363,7 @@ prompt_use_staking() { ;; esac done + echo "" } # Function to set or add a variable in the .env file and export it @@ -316,6 +405,8 @@ agent_address_path="$store/agent_address.txt" service_id_path="$store/service_id.txt" service_safe_address_path="$store/service_safe_address.txt" store_readme_path="$store/README.txt" +use_password=false +password_argument="" zero_address="0x0000000000000000000000000000000000000000" # Function to create the .trader_runner storage @@ -325,6 +416,8 @@ create_storage() { echo "This is the first run of the script. The script will generate new operator and agent instance addresses." echo "" + ask_confirm_password + mkdir "../$store" # Generate README.txt file @@ -353,7 +446,7 @@ create_storage() { echo -n "$rpc" > "../$rpc_path" # Generate the owner/operator's key - poetry run autonomy generate-key -n1 ethereum + poetry run autonomy generate-key -n1 ethereum $password_argument mv "$keys_json" "../$operator_keys_file" operator_address=$(get_address "../$operator_keys_file") operator_pkey=$(get_private_key "../$operator_keys_file") @@ -362,7 +455,7 @@ create_storage() { echo "(The same address will be used as the service owner.)" # Generate the agent's key - poetry run autonomy generate-key -n1 ethereum + poetry run autonomy generate-key -n1 ethereum $password_argument mv "$keys_json" "../$keys_json_path" agent_address=$(get_address "../$keys_json_path") agent_pkey=$(get_private_key "../$keys_json_path") @@ -434,6 +527,7 @@ try_read_storage() { AGENT_ID=14 fi dotenv_set_key "$env_file_path" "AGENT_ID" "$AGENT_ID" + ask_password_if_needed else first_run=true fi @@ -590,6 +684,8 @@ then create_storage "$rpc" fi +validate_password + echo "" echo "-----------------------------------------" echo "Checking Autonolas Protocol service state" @@ -630,7 +726,7 @@ then --skip-hash-check \ --use-custom-chain \ service packages/valory/services/$directory/ \ - --key \"../$operator_pkey_path\" \ + --key \"../$operator_pkey_path\" $password_argument\ --nft $nft \ -a $AGENT_ID \ -n $n_agents \ @@ -711,13 +807,13 @@ if [ "$local_service_hash" != "$remote_service_hash" ]; then echo "" service_safe_address=$(<"../$service_safe_address_path") - current_safe_owners=$(poetry run python "../scripts/get_safe_owners.py" "$service_safe_address" "../$agent_pkey_path" "$rpc" | awk '{gsub(/"/, "\047", $0); print $0}') + current_safe_owners=$(poetry run python "../scripts/get_safe_owners.py" "$service_safe_address" "../$agent_pkey_path" "$rpc" $password_argument | awk '{gsub(/"/, "\047", $0); print $0}') # transfer the ownership of the Safe from the agent to the service owner # (in a live service, this should be done by sending a 0 DAI transfer to its Safe) if [[ "$(get_on_chain_service_state "$service_id")" == "DEPLOYED" && "$current_safe_owners" == "['$agent_address']" ]]; then echo "[Agent instance] Swapping Safe owner..." - poetry run python "../scripts/swap_safe_owner.py" "$service_safe_address" "../$agent_pkey_path" "$operator_address" "$rpc" + poetry run python "../scripts/swap_safe_owner.py" "$service_safe_address" "../$agent_pkey_path" "$operator_address" "$rpc" $password_argument if [[ $? -ne 0 ]]; then echo "Swapping Safe owner failed." exit 1 @@ -731,7 +827,7 @@ if [ "$local_service_hash" != "$remote_service_hash" ]; then poetry run autonomy service \ --use-custom-chain \ terminate "$service_id" \ - --key "../$operator_pkey_path" + --key "../$operator_pkey_path" $password_argument ) if [[ $? -ne 0 ]]; then echo "Terminating service failed.\n$output" @@ -747,7 +843,7 @@ if [ "$local_service_hash" != "$remote_service_hash" ]; then poetry run autonomy service \ --use-custom-chain \ unbond "$service_id" \ - --key "../$operator_pkey_path" + --key "../$operator_pkey_path" $password_argument ) if [[ $? -ne 0 ]]; then echo "Unbonding service failed.\n$output" @@ -763,14 +859,14 @@ if [ "$local_service_hash" != "$remote_service_hash" ]; then export cmd="" if [ "${USE_STAKING}" = true ]; then cost_of_bonding=$olas_balance_required_to_bond - poetry run python "../scripts/update_service.py" "../$operator_pkey_path" "$nft" "$AGENT_ID" "$service_id" "$CUSTOM_OLAS_ADDRESS" "$cost_of_bonding" "packages/valory/services/trader/" "$rpc" + poetry run python "../scripts/update_service.py" "../$operator_pkey_path" "$nft" "$AGENT_ID" "$service_id" "$CUSTOM_OLAS_ADDRESS" "$cost_of_bonding" "packages/valory/services/trader/" "$rpc" $password_argument else cost_of_bonding=$xdai_balance_required_to_bond cmd="poetry run autonomy mint \ --skip-hash-check \ --use-custom-chain \ service packages/valory/services/trader/ \ - --key \"../$operator_pkey_path\" \ + --key \"../$operator_pkey_path\" $password_argument \ --nft $nft \ -a $AGENT_ID \ -n $n_agents \ @@ -803,7 +899,7 @@ fi # activate service if [ "$(get_on_chain_service_state "$service_id")" == "PRE_REGISTRATION" ]; then echo "[Service owner] Activating registration for on-chain service $service_id..." - export cmd="poetry run autonomy service --use-custom-chain activate --key "../$operator_pkey_path" "$service_id"" + export cmd="poetry run autonomy service --use-custom-chain activate --key "../$operator_pkey_path" $password_argument "$service_id"" if [ "${USE_STAKING}" = true ]; then minimum_olas_balance=$($PYTHON_CMD -c "print(int($olas_balance_required_to_bond) + int($olas_balance_required_to_stake))") echo "Your service is using staking. Therefore, you need to provide a total of $(wei_to_dai "$minimum_olas_balance") OLAS to your owner/operator's address:" @@ -825,7 +921,7 @@ fi # register agent instance if [ "$(get_on_chain_service_state "$service_id")" == "ACTIVE_REGISTRATION" ]; then echo "[Operator] Registering agent instance for on-chain service $service_id..." - export cmd="poetry run autonomy service --use-custom-chain register --key "../$operator_pkey_path" "$service_id" -a $AGENT_ID -i "$agent_address"" + export cmd="poetry run autonomy service --use-custom-chain register --key "../$operator_pkey_path" $password_argument "$service_id" -a $AGENT_ID -i "$agent_address"" if [ "${USE_STAKING}" = true ]; then cmd+=" --token $CUSTOM_OLAS_ADDRESS" @@ -844,7 +940,7 @@ service_state="$(get_on_chain_service_state "$service_id")" multisig_address="$(get_multisig_address "$service_id")" if ( [ "$first_run" = "true" ] || [ "$multisig_address" == "$zero_address" ] ) && [ "$service_state" == "FINISHED_REGISTRATION" ]; then echo "[Service owner] Deploying on-chain service $service_id..." - output=$(poetry run autonomy service --use-custom-chain deploy "$service_id" --key "../$operator_pkey_path") + output=$(poetry run autonomy service --use-custom-chain deploy "$service_id" --key "../$operator_pkey_path" $password_argument) if [[ $? -ne 0 ]]; then echo "Deploying service failed.\n$output" echo "Please, delete or rename the ./trader folder and try re-run this script again." @@ -852,7 +948,7 @@ if ( [ "$first_run" = "true" ] || [ "$multisig_address" == "$zero_address" ] ) & fi elif [ "$service_state" == "FINISHED_REGISTRATION" ]; then echo "[Service owner] Deploying on-chain service $service_id..." - output=$(poetry run autonomy service --use-custom-chain deploy "$service_id" --key "../$operator_pkey_path" --reuse-multisig) + output=$(poetry run autonomy service --use-custom-chain deploy "$service_id" --key "../$operator_pkey_path" $password_argument --reuse-multisig) if [[ $? -ne 0 ]]; then echo "Deploying service failed.\n$output" echo "Please, delete or rename the ./trader folder and try re-run this script again." @@ -955,11 +1051,10 @@ else cd $service_dir # Build the image poetry run autonomy build-image - cp ../../$keys_json_path $keys_json fi # Build the deployment with a single agent -poetry run autonomy deploy build --n $n_agents -ltm +export OPEN_AUTONOMY_PRIVATE_KEY_PASSWORD="$password" && poetry run autonomy deploy build "../../$keys_json_path" --n $n_agents -ltm cd .. @@ -969,4 +1064,4 @@ add_volume_to_service "$PWD/trader_service/abci_build/docker-compose.yaml" "trad sudo chown -R $(whoami) "$PWD/../$store/" # Run the deployment -poetry run autonomy deploy run --build-dir $directory --detach +export OPEN_AUTONOMY_PRIVATE_KEY_PASSWORD="$password" && poetry run autonomy deploy run --build-dir "$directory" --detach diff --git a/scripts/change_keys_json_password.py b/scripts/change_keys_json_password.py new file mode 100644 index 00000000..5c559f3e --- /dev/null +++ b/scripts/change_keys_json_password.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2022-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Changes the key files password of the trader store.""" + +import argparse +import json +import tempfile +from pathlib import Path + +from aea.crypto.helpers import DecryptError, KeyIsIncorrect +from aea_ledger_ethereum.ethereum import EthereumCrypto + + +def _change_keys_json_password( + keys_json_path: Path, pkey_txt_path: Path, current_password: str, new_password: str +) -> None: # pylint: disable=too-many-arguments + keys_json_reencrypeted = [] + keys = json.load(keys_json_path.open("r")) + + with tempfile.TemporaryDirectory() as temp_dir: + for idx, key in enumerate(keys): + temp_file = Path(temp_dir, str(idx)) + temp_file.open("w+", encoding="utf-8").write(str(key["private_key"])) + try: + crypto = EthereumCrypto.load_private_key_from_path( + str(temp_file), password=current_password + ) + + if new_password: + new_private_key_value = ( + f"{json.dumps(crypto.encrypt(new_password))}" + ) + else: + print( + "WARNING: No new password provided. Files will be not encrypted." + ) + new_private_key_value = crypto.key.hex() + + keys_json_reencrypeted.append( + { + "address": crypto.address, + "private_key": new_private_key_value, + } + ) + json.dump(keys_json_reencrypeted, keys_json_path.open("w+"), indent=2) + print(f"Changed password {keys_json_path}") + + with open(pkey_txt_path, "w", encoding="utf-8") as file: + if new_private_key_value.startswith("0x"): + file.write(new_private_key_value[2:]) + else: + file.write(new_private_key_value) + print(f"Ovewritten {pkey_txt_path}") + except (DecryptError, KeyIsIncorrect): + print("Bad password provided.") + except json.decoder.JSONDecodeError: + print( + "Wrong key file format. If key file is not encrypted, do not provide '--current_password' parameter" + ) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Change key files password.") + parser.add_argument( + "store_path", type=str, help="Path to the trader store directory." + ) + parser.add_argument( + "--current_password", + type=str, + help="Current password. If not provided, it is assumed files are not encrypted.", + ) + parser.add_argument( + "--new_password", + type=str, + help="New password. If not provided, it will decrypt key files.", + ) + args = parser.parse_args() + + for json_file, pkey_file in ( + ("keys", "agent_pkey"), + ("operator_keys", "operator_pkey"), + ): + _change_keys_json_password( + Path(args.store_path, f"{json_file}.json"), + Path(args.store_path, f"{pkey_file}.txt"), + args.current_password, + args.new_password, + ) + print("") diff --git a/scripts/is_keys_json_password_valid.py b/scripts/is_keys_json_password_valid.py new file mode 100644 index 00000000..08f583d7 --- /dev/null +++ b/scripts/is_keys_json_password_valid.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2022-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Checks if the provided password is valid for a keys.json file.""" + +import argparse +import json +import tempfile +import traceback +from pathlib import Path + +from aea.crypto.helpers import DecryptError, KeyIsIncorrect +from aea_ledger_ethereum.ethereum import EthereumCrypto + + +def _is_keys_json_password_valid( + keys_json_path: Path, password: str, debug: bool +) -> bool: + keys = json.load(keys_json_path.open("r")) + + with tempfile.TemporaryDirectory() as temp_dir: + for idx, key in enumerate(keys): + temp_file = Path(temp_dir, str(idx)) + temp_file.open("w+", encoding="utf-8").write(str(key["private_key"])) + + try: + EthereumCrypto.load_private_key_from_path( + str(temp_file), password=password + ) + except ( + DecryptError, + json.decoder.JSONDecodeError, + KeyIsIncorrect, + ): + if debug: + stack_trace = traceback.format_exc() + print(stack_trace) + return False + + return True + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Checks if the provided password is valid for a keys.json file." + ) + parser.add_argument("keys_json_path", type=str, help="Path to the keys.json file.") + parser.add_argument( + "--password", + type=str, + help="Password. If not provided, it assumes keys.json is not password-protected.", + ) + parser.add_argument("--debug", action="store_true", help="Prints debug messages.") + args = parser.parse_args() + print( + _is_keys_json_password_valid( + Path(args.keys_json_path), args.password, args.debug + ) + ) diff --git a/scripts/staking.py b/scripts/staking.py index 096264b1..0dc50ce4 100644 --- a/scripts/staking.py +++ b/scripts/staking.py @@ -68,9 +68,10 @@ help="True if the service should be unstaked, False if it should be staked", default=False, ) + parser.add_argument("--password", type=str, help="Private key password") args = parser.parse_args() ledger_api = EthereumApi(address=args.rpc) - owner_crypto = EthereumCrypto(private_key_path=args.owner_private_key_path) + owner_crypto = EthereumCrypto(private_key_path=args.owner_private_key_path, password=args.password) if args.unstake: if not is_service_staked(ledger_api, args.service_id, args.staking_contract_address): # the service is not staked, so we don't need to do anything diff --git a/scripts/swap_safe_owner.py b/scripts/swap_safe_owner.py index 6b746d81..2c23d3e5 100644 --- a/scripts/swap_safe_owner.py +++ b/scripts/swap_safe_owner.py @@ -77,12 +77,13 @@ def load_contract(ctype: ContractType) -> ContractType: "new_owner_address", type=str, help="Recipient address on the Gnosis chain" ) parser.add_argument("rpc", type=str, help="RPC for the Gnosis chain") + parser.add_argument("--password", type=str, help="Private key password") args = parser.parse_args() ledger_api = EthereumApi(address=args.rpc) current_owner_crypto: EthereumCrypto current_owner_crypto = EthereumCrypto( - private_key_path=args.current_owner_private_key_path + private_key_path=args.current_owner_private_key_path, password=args.password ) owner_cryptos: list[EthereumCrypto] = [current_owner_crypto] diff --git a/scripts/update_service.py b/scripts/update_service.py index 490c9683..b8e64c50 100644 --- a/scripts/update_service.py +++ b/scripts/update_service.py @@ -180,9 +180,10 @@ def update_service( # pylint: disable=too-many-arguments,too-many-locals help="The directory of the service package.", ) parser.add_argument("rpc", type=str, help="RPC for the Gnosis chain") + parser.add_argument("--password", type=str, help="Private key password") args = parser.parse_args() ledger_api = EthereumApi(address=args.rpc) - owner_crypto = EthereumCrypto(private_key_path=args.owner_private_key_path) + owner_crypto = EthereumCrypto(private_key_path=args.owner_private_key_path, password=args.password) update_service( ledger_api=ledger_api, crypto=owner_crypto,