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,