From 1cad64e928964a2a95b829de606b206577838075 Mon Sep 17 00:00:00 2001 From: johnson2427 Date: Mon, 24 Jun 2024 16:00:42 -0500 Subject: [PATCH 01/28] feat: adding import functionality --- ape_aws/client.py | 62 ++++++++++++++++++++++++++++++++++++++++++--- ape_aws/kms/_cli.py | 62 ++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 118 insertions(+), 6 deletions(-) diff --git a/ape_aws/client.py b/ape_aws/client.py index d40e06b..9869a32 100644 --- a/ape_aws/client.py +++ b/ape_aws/client.py @@ -1,8 +1,11 @@ +from cryptography.hazmat.primitives.asymmetric import ec, padding +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.backends import default_backend from datetime import datetime from typing import ClassVar import boto3 # type: ignore[import] -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, ConfigDict class AliasResponse(BaseModel): @@ -15,6 +18,7 @@ class AliasResponse(BaseModel): class KeyBaseModel(BaseModel): alias: str + model_config = ConfigDict(populate_by_name=True) class CreateKeyModel(KeyBaseModel): @@ -67,10 +71,45 @@ class CreateKey(CreateKeyModel): origin: str = Field(default="AWS_KMS", alias="Origin") -class ImportKey(CreateKeyModel): +class ImportKeyRequest(CreateKeyModel): origin: str = Field(default="EXTERNAL", alias="Origin") +class ImportKey(ImportKeyRequest): + key_id: str = Field(default=None, alias="KeyId") + public_key: bytes = Field(default=None, alias="PublicKey") + private_key: bytes = Field( + default=ec.generate_private_key( + ec.SeCP256K1(), + default_backend() + ).private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption(), + ), + alias="PrivateKey", + ) + import_token: bytes = Field(default=None, alias="ImportToken") + + @property + def encrypted_key(self): + if not self.public_key: + raise ValueError("Public key not found") + + serialized_public_key = serialization.load_der_public_key( + self.public_key, + backend=default_backend(), + ) + return serialized_public_key.encrypt( + self.private_key, + padding.OAEP( + mgf=padding.MGF1(hashes.SHA256()), + algorithm=hashes.SHA256(), + label=None, + ) + ) + + class DeleteKey(KeyBaseModel): key_id: str days: int = 30 @@ -103,8 +142,9 @@ def sign(self, key_id, msghash): ) return response.get("Signature") - def create_key(self, key_spec: CreateKey): + def create_key(self, key_spec: CreateKey | ImportKey): response = self.client.create_key(**key_spec.to_aws_dict()) + key_id = response["KeyMetadata"]["KeyId"] self.client.create_alias( AliasName=f"alias/{key_spec.alias}", @@ -131,6 +171,22 @@ def create_key(self, key_spec: CreateKey): ) return key_id + def import_key(self, key_spec: ImportKey): + breakpoint() + return self.client.import_key_material( + KeyId=key_spec.key_id, + ImportToken=key_spec.import_token, + EncryptedKeyMaterial=key_spec.encrypted_key, + ExpirationModel="KEY_MATERIAL_DOES_NOT_EXPIRE", + ) + + def get_parameters(self, key_id: str): + return self.client.get_parameters_for_import( + KeyId=key_id, + WrappingAlgorithm="RSAES_OAEP_SHA_256", + WrappingKeySpec="RSA_2048", + ) + def delete_key(self, key_spec: DeleteKey): self.client.delete_alias(AliasName=key_spec.alias) self.client.schedule_key_deletion(KeyId=key_spec.key_id, PendingWindowInDays=key_spec.days) diff --git a/ape_aws/kms/_cli.py b/ape_aws/kms/_cli.py index c6145e7..e112789 100644 --- a/ape_aws/kms/_cli.py +++ b/ape_aws/kms/_cli.py @@ -1,7 +1,14 @@ +import base64 import click -from ape.cli import ape_cli_context -from ape_aws.client import CreateKey, DeleteKey, kms_client +from ape.cli import ape_cli_context +from ape_aws.client import ( + CreateKey, + DeleteKey, + ImportKeyRequest, + ImportKey, + kms_client, +) @click.group("kms") @@ -54,7 +61,56 @@ def create_key( cli_ctx.logger.success(f"Key created successfully with ID: {key_id}") -# TODO: Add `ape aws kms import` +@kms.command(name="import") +@ape_cli_context() +@click.option( + "-a", + "--admin", + "administrators", + multiple=True, + help="Apply key policy to a list of administrators if applicable, ex. -a ARN1, -a ARN2", + metavar="list[ARN]", +) +@click.option( + "-u", + "--user", + "users", + multiple=True, + help="Apply key policy to a list of users if applicable, ex. -u ARN1, -u ARN2", + metavar="list[ARN]", +) +@click.argument("alias_name") +@click.argument("description") +@click.argument("private_key") +def import_key( + cli_ctx, + alias_name: str, + description: str, + private_key: bytes, + administrators: list[str], + users: list[str], +): + key_spec = ImportKeyRequest( + alias=alias_name, + description=description, + admins=administrators, + users=users, + ) + key_id = kms_client.create_key(key_spec) + create_key_response = kms_client.get_parameters(key_id) + public_key = base64.b64encode(create_key_response["PublicKey"]) + import_token = base64.b64encode(create_key_response["ImportToken"]) + import_key_spec = ImportKey( + **key_spec.model_dump(), + key_id=key_id, + public_key=public_key, + private_key=private_key, + import_token=import_token, + ) + key_id = kms_client.import_key(import_key_spec) + cli_ctx.logger.success(f"Key imported successfully with ID: {key_id}") + + # TODO: Add `ape aws kms sign-message [message]` # TODO: Add `ape aws kms verify-message [message] [hex-signature]` From 736142f7686585f286b86c643e71df995d62bc18 Mon Sep 17 00:00:00 2001 From: johnson2427 Date: Tue, 25 Jun 2024 10:57:31 -0500 Subject: [PATCH 02/28] feat: working import --- ape_aws/client.py | 28 +++++++++++++++------------- ape_aws/kms/_cli.py | 12 +++++++++--- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/ape_aws/client.py b/ape_aws/client.py index 9869a32..bc731ae 100644 --- a/ape_aws/client.py +++ b/ape_aws/client.py @@ -5,7 +5,7 @@ from typing import ClassVar import boto3 # type: ignore[import] -from pydantic import BaseModel, Field, ConfigDict +from pydantic import BaseModel, Field, ConfigDict, field_validator class AliasResponse(BaseModel): @@ -78,19 +78,22 @@ class ImportKeyRequest(CreateKeyModel): class ImportKey(ImportKeyRequest): key_id: str = Field(default=None, alias="KeyId") public_key: bytes = Field(default=None, alias="PublicKey") - private_key: bytes = Field( - default=ec.generate_private_key( - ec.SeCP256K1(), - default_backend() - ).private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.TraditionalOpenSSL, - encryption_algorithm=serialization.NoEncryption(), - ), - alias="PrivateKey", - ) + private_key: bytes | None = Field(default=None, alias="PrivateKey") import_token: bytes = Field(default=None, alias="ImportToken") + @field_validator("private_key") + def validate_private_key(cls, value): + if not isinstance(value, bytes): + return ec.generate_private_key( + ec.SECP256K1(), + default_backend() + ).private_bytes( + encoding=serialization.Encoding.DER, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ) + return value + @property def encrypted_key(self): if not self.public_key: @@ -172,7 +175,6 @@ def create_key(self, key_spec: CreateKey | ImportKey): return key_id def import_key(self, key_spec: ImportKey): - breakpoint() return self.client.import_key_material( KeyId=key_spec.key_id, ImportToken=key_spec.import_token, diff --git a/ape_aws/kms/_cli.py b/ape_aws/kms/_cli.py index e112789..f31155a 100644 --- a/ape_aws/kms/_cli.py +++ b/ape_aws/kms/_cli.py @@ -63,6 +63,13 @@ def create_key( @kms.command(name="import") @ape_cli_context() +@click.option( + "-p", + "--private-key", + "private_key", + multiple=False, + help="The private key to import", +) @click.option( "-a", "--admin", @@ -81,7 +88,6 @@ def create_key( ) @click.argument("alias_name") @click.argument("description") -@click.argument("private_key") def import_key( cli_ctx, alias_name: str, @@ -98,8 +104,8 @@ def import_key( ) key_id = kms_client.create_key(key_spec) create_key_response = kms_client.get_parameters(key_id) - public_key = base64.b64encode(create_key_response["PublicKey"]) - import_token = base64.b64encode(create_key_response["ImportToken"]) + public_key = create_key_response["PublicKey"] + import_token = create_key_response["ImportToken"] import_key_spec = ImportKey( **key_spec.model_dump(), key_id=key_id, From 638e7d89ab67aeea775c49f18f9566d072324d46 Mon Sep 17 00:00:00 2001 From: johnson2427 Date: Tue, 25 Jun 2024 15:25:30 -0500 Subject: [PATCH 03/28] feat: adding functionality to allow for export of keys --- ape_aws/client.py | 52 +++++++++++++++++++++++++++++++++++---------- ape_aws/kms/_cli.py | 1 - 2 files changed, 41 insertions(+), 12 deletions(-) diff --git a/ape_aws/client.py b/ape_aws/client.py index bc731ae..98ab26e 100644 --- a/ape_aws/client.py +++ b/ape_aws/client.py @@ -1,6 +1,8 @@ from cryptography.hazmat.primitives.asymmetric import ec, padding from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.backends import default_backend +from eth_account import Account + from datetime import datetime from typing import ClassVar @@ -87,24 +89,52 @@ def validate_private_key(cls, value): return ec.generate_private_key( ec.SECP256K1(), default_backend() - ).private_bytes( - encoding=serialization.Encoding.DER, - format=serialization.PrivateFormat.PKCS8, - encryption_algorithm=serialization.NoEncryption(), ) + if value.startswith('0x'): + return value[2:] return value @property - def encrypted_key(self): - if not self.public_key: - raise ValueError("Public key not found") + def get_account(self): + return Account.privateKeyToAccount(self.private_key) + + @property + def private_key_bin(self): + """ + Returns the private key in binary format + This is required for the `boto3.client.import_key_material` method + """ + return self.private_key.private_bytes( + encoding=serialization.Encoding.DER, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ) - serialized_public_key = serialization.load_der_public_key( + @property + def private_key_pem(self): + """ + Returns the private key in PEM format for use in outside applications. + """ + return self.private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption(), + ) + + @property + def public_key_der(self): + return serialization.load_der_public_key( self.public_key, backend=default_backend(), ) - return serialized_public_key.encrypt( - self.private_key, + + @property + def encrypted_private_key(self): + if not self.public_key: + raise ValueError("Public key not found") + + return self.public_key_der.encrypt( + self.private_key_bin, padding.OAEP( mgf=padding.MGF1(hashes.SHA256()), algorithm=hashes.SHA256(), @@ -178,7 +208,7 @@ def import_key(self, key_spec: ImportKey): return self.client.import_key_material( KeyId=key_spec.key_id, ImportToken=key_spec.import_token, - EncryptedKeyMaterial=key_spec.encrypted_key, + EncryptedKeyMaterial=key_spec.encrypted_private_key, ExpirationModel="KEY_MATERIAL_DOES_NOT_EXPIRE", ) diff --git a/ape_aws/kms/_cli.py b/ape_aws/kms/_cli.py index f31155a..adb8f13 100644 --- a/ape_aws/kms/_cli.py +++ b/ape_aws/kms/_cli.py @@ -1,4 +1,3 @@ -import base64 import click from ape.cli import ape_cli_context From 0cc8bb99349d08d4c7bc0a9634febf7cad131b0e Mon Sep 17 00:00:00 2001 From: johnson2427 Date: Wed, 26 Jun 2024 16:47:15 -0500 Subject: [PATCH 04/28] feat: adding private key to cache --- ape_aws/accounts.py | 56 +++++++++++++++++++++++++++++++++++++++++---- ape_aws/client.py | 20 ++++++++++++++-- ape_aws/kms/_cli.py | 15 +++++++++++- 3 files changed, 83 insertions(+), 8 deletions(-) diff --git a/ape_aws/accounts.py b/ape_aws/accounts.py index 8c6ddde..33dd256 100644 --- a/ape_aws/accounts.py +++ b/ape_aws/accounts.py @@ -1,37 +1,78 @@ +from json import dumps from functools import cached_property +from pathlib import Path from typing import Any, Iterator, Optional from ape.api.accounts import AccountAPI, AccountContainerAPI, TransactionAPI from ape.types import AddressType, MessageSignature, SignableMessage, TransactionSignature +from ape.utils.validators import _validate_account_passphrase + from eth_account._utils.legacy_transactions import serializable_unsigned_transaction_from_dict from eth_account.messages import _hash_eip191_message, encode_defunct +from eth_account import Account as EthAccount from eth_pydantic_types import HexBytes from eth_typing import Hash32 -from eth_utils import keccak, to_checksum_address +from eth_utils import keccak, to_checksum_address, to_bytes from .client import kms_client from .utils import _convert_der_to_rsv class AwsAccountContainer(AccountContainerAPI): + loaded_accounts: dict[str, "KmsAccount"] = {} + + def model_post_init(self, __context: Any): + print("Initializing AWS KMS Account Container") + print([acc.alias for acc in self.accounts]) + + @property + def _keyfiles(self) -> list[Path]: + return [file for file in self.data_folder.glob("*.json")] + @property def aliases(self) -> Iterator[str]: - return map(lambda x: x.alias, kms_client.raw_aliases) + return map(lambda x: x.alias.replace("alias/", ""), kms_client.raw_aliases) def __len__(self) -> int: return len(kms_client.raw_aliases) @property def accounts(self) -> Iterator[AccountAPI]: + def _load_account(key_alias, key_id, key_arn) -> Iterator[AccountAPI]: + filename = f"{key_alias}.json" + keyfile = self.data_folder.joinpath(filename) + if filename not in self._keyfiles: + self.loaded_accounts[keyfile.stem] = KmsAccount( + key_alias=key_alias, + key_id=key_id, + key_arn=key_arn, + ) + keyfile.write_text( + self.loaded_accounts[keyfile.stem].dump_to_json() + ) + return self.loaded_accounts[keyfile.stem] return map( - lambda x: KmsAccount( - key_alias=x.alias, + lambda x: _load_account( + key_alias=x.alias.replace("alias/", ""), key_id=x.key_id, key_arn=x.arn, ), kms_client.raw_aliases, ) + def add_private_key(self, alias, passphrase, private_key): + kms_account = self.loaded_accounts[alias] + _validate_account_passphrase(passphrase) + account = EthAccount.from_key(to_bytes(hexstr=private_key)) + keyfile = self.data_folder.joinpath(f"{alias}.json") + account = EthAccount.encrypt(account.key, passphrase) + model = kms_account.model_dump() + model["address"] = kms_account.address + del account["address"] + model.update(account) + keyfile.write_text(dumps(model, indent=4)) + print("Key cached successfully") + return class KmsAccount(AccountAPI): key_alias: str @@ -40,7 +81,7 @@ class KmsAccount(AccountAPI): @property def alias(self) -> str: - return self.key_alias.replace("alias/", "") + return self.key_alias @property def public_key(self): @@ -105,3 +146,8 @@ def sign_transaction(self, txn: TransactionAPI, **signer_options) -> Optional[Tr ) return txn + + def dump_to_json(self, indent: int = 4): + model = self.model_dump() + model["address"] = self.address + return dumps(model, indent=indent) diff --git a/ape_aws/client.py b/ape_aws/client.py index 98ab26e..fccdb12 100644 --- a/ape_aws/client.py +++ b/ape_aws/client.py @@ -1,13 +1,17 @@ +from ape.utils.basemodel import ManagerAccessMixin + from cryptography.hazmat.primitives.asymmetric import ec, padding from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.backends import default_backend from eth_account import Account +from eth_utils import to_bytes from datetime import datetime from typing import ClassVar +from pydantic import BaseModel, Field, ConfigDict, field_validator import boto3 # type: ignore[import] -from pydantic import BaseModel, Field, ConfigDict, field_validator +import json class AliasResponse(BaseModel): @@ -91,13 +95,17 @@ def validate_private_key(cls, value): default_backend() ) if value.startswith('0x'): - return value[2:] + value = bytes.fromhex(value[2:]) return value @property def get_account(self): return Account.privateKeyToAccount(self.private_key) + @property + def private_key_hex(self): + return self.private_key.private_numbers().private_value.to_bytes(32, "big").hex() + @property def private_key_bin(self): """ @@ -142,6 +150,14 @@ def encrypted_private_key(self): ) ) + def import_account_from_private_key(self, passphrase: str): + account = Account.from_key(to_bytes(hexstr=self.private_key_hex)) + path = ManagerAccessMixin.account_manager.containers["accounts"].data_folder.joinpath( + f"{self.alias}.json" + ) + path.write_text(json.dumps(Account.encrypt(account.key, passphrase))) + return KmsAccount() + class DeleteKey(KeyBaseModel): key_id: str diff --git a/ape_aws/kms/_cli.py b/ape_aws/kms/_cli.py index adb8f13..33a9f61 100644 --- a/ape_aws/kms/_cli.py +++ b/ape_aws/kms/_cli.py @@ -1,6 +1,7 @@ import click from ape.cli import ape_cli_context +from ape_aws.accounts import AwsAccountContainer, KmsAccount from ape_aws.client import ( CreateKey, DeleteKey, @@ -95,6 +96,14 @@ def import_key( administrators: list[str], users: list[str], ): + def ask_for_passphrase(): + return click.prompt( + "Create Passphrase to encrypt account", + hide_input=True, + confirmation_prompt=True, + ) + + passphrase = ask_for_passphrase() key_spec = ImportKeyRequest( alias=alias_name, description=description, @@ -112,8 +121,12 @@ def import_key( private_key=private_key, import_token=import_token, ) - key_id = kms_client.import_key(import_key_spec) + response = kms_client.import_key(import_key_spec) + if response["ResponseMetadata"]["HTTPStatusCode"] != 200: + cli_ctx.abort("Key failed to import into KMS") cli_ctx.logger.success(f"Key imported successfully with ID: {key_id}") + aws_account_container = AwsAccountContainer(name="aws", account_type=KmsAccount) + aws_account_container.add_private_key(alias_name, passphrase, import_key_spec.private_key_hex) # TODO: Add `ape aws kms sign-message [message]` From a4a83cd18e6489951093349e528dc4c8e824bcdd Mon Sep 17 00:00:00 2001 From: johnson2427 Date: Thu, 27 Jun 2024 09:37:03 -0500 Subject: [PATCH 05/28] feat: add purge to delete key from hidden ape folder --- ape_aws/accounts.py | 10 ++++++++++ ape_aws/client.py | 2 +- ape_aws/kms/_cli.py | 16 +++++++++++++--- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/ape_aws/accounts.py b/ape_aws/accounts.py index 33dd256..6e842b0 100644 --- a/ape_aws/accounts.py +++ b/ape_aws/accounts.py @@ -74,6 +74,16 @@ def add_private_key(self, alias, passphrase, private_key): print("Key cached successfully") return + def delete_account(self, alias): + alias = alias.replace("alias/", "") + keyfile = self.data_folder.joinpath(f"{alias}.json") + if keyfile.exists(): + keyfile.unlink() + print(f"Key {alias} deleted successfully") + else: + print(f"Key {alias} not found") + + class KmsAccount(AccountAPI): key_alias: str key_id: str diff --git a/ape_aws/client.py b/ape_aws/client.py index fccdb12..36c0f6b 100644 --- a/ape_aws/client.py +++ b/ape_aws/client.py @@ -28,7 +28,7 @@ class KeyBaseModel(BaseModel): class CreateKeyModel(KeyBaseModel): - description: str = Field(alias="Description") + description: str | None = Field(default=None, alias="Description") policy: str | None = Field(default=None, alias="Policy") key_usage: str = Field(default="SIGN_VERIFY", alias="KeyUsage") key_spec: str = Field(default="ECC_SECG_P256K1", alias="KeySpec") diff --git a/ape_aws/kms/_cli.py b/ape_aws/kms/_cli.py index 33a9f61..2877385 100644 --- a/ape_aws/kms/_cli.py +++ b/ape_aws/kms/_cli.py @@ -86,15 +86,21 @@ def create_key( help="Apply key policy to a list of users if applicable, ex. -u ARN1, -u ARN2", metavar="list[ARN]", ) +@click.option( + "-d", + "--description", + "description", + help="The description of the key you intend to create.", + metavar="str", +) @click.argument("alias_name") -@click.argument("description") def import_key( cli_ctx, alias_name: str, - description: str, private_key: bytes, administrators: list[str], users: list[str], + description: str, ): def ask_for_passphrase(): return click.prompt( @@ -136,8 +142,9 @@ def ask_for_passphrase(): @kms.command(name="delete") @ape_cli_context() @click.argument("alias_name") +@click.option("-p", "--purge", is_flag=True, help="Purge the key from the system") @click.option("-d", "--days", default=30, help="Number of days until key is deactivated") -def schedule_delete_key(cli_ctx, alias_name, days): +def schedule_delete_key(cli_ctx, alias_name, purge, days): if "alias" not in alias_name: alias_name = f"alias/{alias_name}" kms_account = None @@ -150,4 +157,7 @@ def schedule_delete_key(cli_ctx, alias_name, days): delete_key_spec = DeleteKey(alias=alias_name, key_id=kms_account.key_id, days=days) key_alias = kms_client.delete_key(delete_key_spec) + if purge: + aws_account_container = AwsAccountContainer(name="aws", account_type=KmsAccount) + aws_account_container.delete_account(key_alias) cli_ctx.logger.success(f"Key {key_alias} scheduled for deletion in {days} days") From c0e778810b96e3694397086ea0a0a536e922430c Mon Sep 17 00:00:00 2001 From: johnson2427 Date: Thu, 27 Jun 2024 10:34:32 -0500 Subject: [PATCH 06/28] feat: add ability to add your own private key --- ape_aws/client.py | 36 +++++++++++++++++++++++------------- ape_aws/kms/_cli.py | 1 + 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/ape_aws/client.py b/ape_aws/client.py index 36c0f6b..643a549 100644 --- a/ape_aws/client.py +++ b/ape_aws/client.py @@ -84,26 +84,44 @@ class ImportKeyRequest(CreateKeyModel): class ImportKey(ImportKeyRequest): key_id: str = Field(default=None, alias="KeyId") public_key: bytes = Field(default=None, alias="PublicKey") - private_key: bytes | None = Field(default=None, alias="PrivateKey") + private_key: str | bytes | None = Field(default=None, alias="PrivateKey") import_token: bytes = Field(default=None, alias="ImportToken") @field_validator("private_key") def validate_private_key(cls, value): - if not isinstance(value, bytes): + if not value: return ec.generate_private_key( ec.SECP256K1(), default_backend() ) if value.startswith('0x'): - value = bytes.fromhex(value[2:]) + value = value[2:] return value @property def get_account(self): return Account.privateKeyToAccount(self.private_key) + @property + def ec_private_key(self): + loaded_key = self.private_key + if isinstance(loaded_key, bytes): + loaded_key = ec.derive_private_key( + int(self.private_key, 16), ec.SECP256K1() + ) + elif isinstance(loaded_key, str): + loaded_key = bytes.fromhex(loaded_key[2:]) + loaded_key = ec.derive_private_key( + int(self.private_key, 16), ec.SECP256K1() + ) + return loaded_key + @property def private_key_hex(self): + if isinstance(self.private_key, str): + return self.private_key + elif isinstance(self.private_key, bytes): + return self.private_key.hex() return self.private_key.private_numbers().private_value.to_bytes(32, "big").hex() @property @@ -112,7 +130,7 @@ def private_key_bin(self): Returns the private key in binary format This is required for the `boto3.client.import_key_material` method """ - return self.private_key.private_bytes( + return self.ec_private_key.private_bytes( encoding=serialization.Encoding.DER, format=serialization.PrivateFormat.PKCS8, encryption_algorithm=serialization.NoEncryption(), @@ -123,7 +141,7 @@ def private_key_pem(self): """ Returns the private key in PEM format for use in outside applications. """ - return self.private_key.private_bytes( + return self.ec_private_key.private_bytes( encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.TraditionalOpenSSL, encryption_algorithm=serialization.NoEncryption(), @@ -150,14 +168,6 @@ def encrypted_private_key(self): ) ) - def import_account_from_private_key(self, passphrase: str): - account = Account.from_key(to_bytes(hexstr=self.private_key_hex)) - path = ManagerAccessMixin.account_manager.containers["accounts"].data_folder.joinpath( - f"{self.alias}.json" - ) - path.write_text(json.dumps(Account.encrypt(account.key, passphrase))) - return KmsAccount() - class DeleteKey(KeyBaseModel): key_id: str diff --git a/ape_aws/kms/_cli.py b/ape_aws/kms/_cli.py index 2877385..29ed97e 100644 --- a/ape_aws/kms/_cli.py +++ b/ape_aws/kms/_cli.py @@ -127,6 +127,7 @@ def ask_for_passphrase(): private_key=private_key, import_token=import_token, ) + breakpoint() response = kms_client.import_key(import_key_spec) if response["ResponseMetadata"]["HTTPStatusCode"] != 200: cli_ctx.abort("Key failed to import into KMS") From 9b7ef46415b8d9c1e208f7a3c5291d34a8650cf6 Mon Sep 17 00:00:00 2001 From: johnson2427 Date: Thu, 27 Jun 2024 10:36:04 -0500 Subject: [PATCH 07/28] fix: remove breakpoint --- ape_aws/kms/_cli.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ape_aws/kms/_cli.py b/ape_aws/kms/_cli.py index 29ed97e..2877385 100644 --- a/ape_aws/kms/_cli.py +++ b/ape_aws/kms/_cli.py @@ -127,7 +127,6 @@ def ask_for_passphrase(): private_key=private_key, import_token=import_token, ) - breakpoint() response = kms_client.import_key(import_key_spec) if response["ResponseMetadata"]["HTTPStatusCode"] != 200: cli_ctx.abort("Key failed to import into KMS") From ce1a88ec76415caf3efc4ed5fd8ae371e430a76c Mon Sep 17 00:00:00 2001 From: johnson2427 Date: Thu, 27 Jun 2024 12:04:04 -0500 Subject: [PATCH 08/28] refactor: linting issues --- ape_aws/accounts.py | 5 ++--- ape_aws/client.py | 17 +++++------------ 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/ape_aws/accounts.py b/ape_aws/accounts.py index 6e842b0..8d96f99 100644 --- a/ape_aws/accounts.py +++ b/ape_aws/accounts.py @@ -47,10 +47,9 @@ def _load_account(key_alias, key_id, key_arn) -> Iterator[AccountAPI]: key_id=key_id, key_arn=key_arn, ) - keyfile.write_text( - self.loaded_accounts[keyfile.stem].dump_to_json() - ) + keyfile.write_text(self.loaded_accounts[keyfile.stem].dump_to_json()) return self.loaded_accounts[keyfile.stem] + return map( lambda x: _load_account( key_alias=x.alias.replace("alias/", ""), diff --git a/ape_aws/client.py b/ape_aws/client.py index 643a549..a9e863d 100644 --- a/ape_aws/client.py +++ b/ape_aws/client.py @@ -90,11 +90,8 @@ class ImportKey(ImportKeyRequest): @field_validator("private_key") def validate_private_key(cls, value): if not value: - return ec.generate_private_key( - ec.SECP256K1(), - default_backend() - ) - if value.startswith('0x'): + return ec.generate_private_key(ec.SECP256K1(), default_backend()) + if value.startswith("0x"): value = value[2:] return value @@ -106,14 +103,10 @@ def get_account(self): def ec_private_key(self): loaded_key = self.private_key if isinstance(loaded_key, bytes): - loaded_key = ec.derive_private_key( - int(self.private_key, 16), ec.SECP256K1() - ) + loaded_key = ec.derive_private_key(int(self.private_key, 16), ec.SECP256K1()) elif isinstance(loaded_key, str): loaded_key = bytes.fromhex(loaded_key[2:]) - loaded_key = ec.derive_private_key( - int(self.private_key, 16), ec.SECP256K1() - ) + loaded_key = ec.derive_private_key(int(self.private_key, 16), ec.SECP256K1()) return loaded_key @property @@ -165,7 +158,7 @@ def encrypted_private_key(self): mgf=padding.MGF1(hashes.SHA256()), algorithm=hashes.SHA256(), label=None, - ) + ), ) From 2ca60741bd1c80ca7c84058070813f30134e0d6e Mon Sep 17 00:00:00 2001 From: johnson2427 Date: Thu, 27 Jun 2024 12:06:15 -0500 Subject: [PATCH 09/28] fix: add cryptography to requirements --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 8950561..6a2a10b 100644 --- a/setup.py +++ b/setup.py @@ -57,6 +57,7 @@ "boto3>=1.34.79,<2", "eth-ape>=0.8.2,<0.9", "ecdsa>=0.19.0,<1", + "cryptography>=37.0.4,<38", ], # NOTE: Add 3rd party libraries here entry_points={"ape_cli_subcommands": ["ape_aws=ape_aws._cli:cli"]}, python_requires=">=3.7,<4", From 87d6879cc9e11396ff5f2f63decb9fc17422e14a Mon Sep 17 00:00:00 2001 From: johnson2427 Date: Thu, 27 Jun 2024 12:08:12 -0500 Subject: [PATCH 10/28] refactor: remove unused imports --- ape_aws/client.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/ape_aws/client.py b/ape_aws/client.py index a9e863d..38b1a6d 100644 --- a/ape_aws/client.py +++ b/ape_aws/client.py @@ -1,17 +1,13 @@ -from ape.utils.basemodel import ManagerAccessMixin - from cryptography.hazmat.primitives.asymmetric import ec, padding from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.backends import default_backend from eth_account import Account -from eth_utils import to_bytes from datetime import datetime from typing import ClassVar from pydantic import BaseModel, Field, ConfigDict, field_validator import boto3 # type: ignore[import] -import json class AliasResponse(BaseModel): From 7bc9b2542a7cacba8604e0d43f3662d74177e129 Mon Sep 17 00:00:00 2001 From: johnson2427 Date: Thu, 27 Jun 2024 12:09:43 -0500 Subject: [PATCH 11/28] refactor: isort --- ape_aws/accounts.py | 7 +++---- ape_aws/client.py | 11 +++++------ ape_aws/kms/_cli.py | 10 ++-------- 3 files changed, 10 insertions(+), 18 deletions(-) diff --git a/ape_aws/accounts.py b/ape_aws/accounts.py index 8d96f99..f3c042f 100644 --- a/ape_aws/accounts.py +++ b/ape_aws/accounts.py @@ -1,18 +1,17 @@ -from json import dumps from functools import cached_property +from json import dumps from pathlib import Path from typing import Any, Iterator, Optional from ape.api.accounts import AccountAPI, AccountContainerAPI, TransactionAPI from ape.types import AddressType, MessageSignature, SignableMessage, TransactionSignature from ape.utils.validators import _validate_account_passphrase - +from eth_account import Account as EthAccount from eth_account._utils.legacy_transactions import serializable_unsigned_transaction_from_dict from eth_account.messages import _hash_eip191_message, encode_defunct -from eth_account import Account as EthAccount from eth_pydantic_types import HexBytes from eth_typing import Hash32 -from eth_utils import keccak, to_checksum_address, to_bytes +from eth_utils import keccak, to_bytes, to_checksum_address from .client import kms_client from .utils import _convert_der_to_rsv diff --git a/ape_aws/client.py b/ape_aws/client.py index 38b1a6d..f347109 100644 --- a/ape_aws/client.py +++ b/ape_aws/client.py @@ -1,13 +1,12 @@ -from cryptography.hazmat.primitives.asymmetric import ec, padding -from cryptography.hazmat.primitives import hashes, serialization -from cryptography.hazmat.backends import default_backend -from eth_account import Account - from datetime import datetime from typing import ClassVar -from pydantic import BaseModel, Field, ConfigDict, field_validator import boto3 # type: ignore[import] +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import ec, padding +from eth_account import Account +from pydantic import BaseModel, ConfigDict, Field, field_validator class AliasResponse(BaseModel): diff --git a/ape_aws/kms/_cli.py b/ape_aws/kms/_cli.py index 2877385..a52ba14 100644 --- a/ape_aws/kms/_cli.py +++ b/ape_aws/kms/_cli.py @@ -1,14 +1,8 @@ import click - from ape.cli import ape_cli_context + from ape_aws.accounts import AwsAccountContainer, KmsAccount -from ape_aws.client import ( - CreateKey, - DeleteKey, - ImportKeyRequest, - ImportKey, - kms_client, -) +from ape_aws.client import CreateKey, DeleteKey, ImportKey, ImportKeyRequest, kms_client @click.group("kms") From 881612d1d2e9a74b28fbf85e0630a6b82ef8f01f Mon Sep 17 00:00:00 2001 From: johnson2427 Date: Thu, 27 Jun 2024 12:18:25 -0500 Subject: [PATCH 12/28] fix: create_key expected inputs --- ape_aws/accounts.py | 2 +- ape_aws/client.py | 2 +- ape_aws/kms/_cli.py | 9 +++++++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/ape_aws/accounts.py b/ape_aws/accounts.py index f3c042f..ae01933 100644 --- a/ape_aws/accounts.py +++ b/ape_aws/accounts.py @@ -37,7 +37,7 @@ def __len__(self) -> int: @property def accounts(self) -> Iterator[AccountAPI]: - def _load_account(key_alias, key_id, key_arn) -> Iterator[AccountAPI]: + def _load_account(key_alias, key_id, key_arn) -> AccountAPI: filename = f"{key_alias}.json" keyfile = self.data_folder.joinpath(filename) if filename not in self._keyfiles: diff --git a/ape_aws/client.py b/ape_aws/client.py index f347109..30154db 100644 --- a/ape_aws/client.py +++ b/ape_aws/client.py @@ -189,7 +189,7 @@ def sign(self, key_id, msghash): ) return response.get("Signature") - def create_key(self, key_spec: CreateKey | ImportKey): + def create_key(self, key_spec: CreateKey | ImportKeyRequest): response = self.client.create_key(**key_spec.to_aws_dict()) key_id = response["KeyMetadata"]["KeyId"] diff --git a/ape_aws/kms/_cli.py b/ape_aws/kms/_cli.py index a52ba14..f0394ba 100644 --- a/ape_aws/kms/_cli.py +++ b/ape_aws/kms/_cli.py @@ -28,14 +28,19 @@ def kms(): help="Apply key policy to a list of users if applicable, ex. -u ARN1, -u ARN2", metavar="list[ARN]", ) +@click.option( + "-d", + "--description", + "description", + help="The description of the key you intend to create.", +) @click.argument("alias_name") -@click.argument("description") def create_key( cli_ctx, alias_name: str, - description: str, administrators: list[str], users: list[str], + description: str, ): """ Create an Ethereum Private Key in AWS KmsAccount From 5062fe47e03a58ea3c0237dea1640cffa7df62d5 Mon Sep 17 00:00:00 2001 From: johnson2427 Date: Thu, 27 Jun 2024 12:25:54 -0500 Subject: [PATCH 13/28] fix: remove mypy errors --- ape_aws/kms/_cli.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ape_aws/kms/_cli.py b/ape_aws/kms/_cli.py index f0394ba..86c152f 100644 --- a/ape_aws/kms/_cli.py +++ b/ape_aws/kms/_cli.py @@ -111,7 +111,7 @@ def ask_for_passphrase(): passphrase = ask_for_passphrase() key_spec = ImportKeyRequest( alias=alias_name, - description=description, + description=description, # type: ignore admins=administrators, users=users, ) @@ -121,10 +121,10 @@ def ask_for_passphrase(): import_token = create_key_response["ImportToken"] import_key_spec = ImportKey( **key_spec.model_dump(), - key_id=key_id, - public_key=public_key, - private_key=private_key, - import_token=import_token, + key_id=key_id, # type: ignore + public_key=public_key, # type: ignore + private_key=private_key, # type: ignore + import_token=import_token, # type: ignore ) response = kms_client.import_key(import_key_spec) if response["ResponseMetadata"]["HTTPStatusCode"] != 200: From a97c8aecce58836473b8118b3c454fd302f09fbc Mon Sep 17 00:00:00 2001 From: johnson2427 Date: Fri, 28 Jun 2024 08:53:26 -0500 Subject: [PATCH 14/28] feat: add ability to input file --- ape_aws/accounts.py | 55 ++------------------------------------------- ape_aws/client.py | 5 ++--- ape_aws/kms/_cli.py | 33 +++++++++------------------ 3 files changed, 14 insertions(+), 79 deletions(-) diff --git a/ape_aws/accounts.py b/ape_aws/accounts.py index ae01933..13c58be 100644 --- a/ape_aws/accounts.py +++ b/ape_aws/accounts.py @@ -1,32 +1,20 @@ from functools import cached_property from json import dumps -from pathlib import Path from typing import Any, Iterator, Optional from ape.api.accounts import AccountAPI, AccountContainerAPI, TransactionAPI from ape.types import AddressType, MessageSignature, SignableMessage, TransactionSignature -from ape.utils.validators import _validate_account_passphrase -from eth_account import Account as EthAccount from eth_account._utils.legacy_transactions import serializable_unsigned_transaction_from_dict from eth_account.messages import _hash_eip191_message, encode_defunct from eth_pydantic_types import HexBytes from eth_typing import Hash32 -from eth_utils import keccak, to_bytes, to_checksum_address +from eth_utils import keccak, to_checksum_address from .client import kms_client from .utils import _convert_der_to_rsv class AwsAccountContainer(AccountContainerAPI): - loaded_accounts: dict[str, "KmsAccount"] = {} - - def model_post_init(self, __context: Any): - print("Initializing AWS KMS Account Container") - print([acc.alias for acc in self.accounts]) - - @property - def _keyfiles(self) -> list[Path]: - return [file for file in self.data_folder.glob("*.json")] @property def aliases(self) -> Iterator[str]: @@ -37,20 +25,8 @@ def __len__(self) -> int: @property def accounts(self) -> Iterator[AccountAPI]: - def _load_account(key_alias, key_id, key_arn) -> AccountAPI: - filename = f"{key_alias}.json" - keyfile = self.data_folder.joinpath(filename) - if filename not in self._keyfiles: - self.loaded_accounts[keyfile.stem] = KmsAccount( - key_alias=key_alias, - key_id=key_id, - key_arn=key_arn, - ) - keyfile.write_text(self.loaded_accounts[keyfile.stem].dump_to_json()) - return self.loaded_accounts[keyfile.stem] - return map( - lambda x: _load_account( + lambda x: KmsAccount( key_alias=x.alias.replace("alias/", ""), key_id=x.key_id, key_arn=x.arn, @@ -58,29 +34,6 @@ def _load_account(key_alias, key_id, key_arn) -> AccountAPI: kms_client.raw_aliases, ) - def add_private_key(self, alias, passphrase, private_key): - kms_account = self.loaded_accounts[alias] - _validate_account_passphrase(passphrase) - account = EthAccount.from_key(to_bytes(hexstr=private_key)) - keyfile = self.data_folder.joinpath(f"{alias}.json") - account = EthAccount.encrypt(account.key, passphrase) - model = kms_account.model_dump() - model["address"] = kms_account.address - del account["address"] - model.update(account) - keyfile.write_text(dumps(model, indent=4)) - print("Key cached successfully") - return - - def delete_account(self, alias): - alias = alias.replace("alias/", "") - keyfile = self.data_folder.joinpath(f"{alias}.json") - if keyfile.exists(): - keyfile.unlink() - print(f"Key {alias} deleted successfully") - else: - print(f"Key {alias} not found") - class KmsAccount(AccountAPI): key_alias: str @@ -155,7 +108,3 @@ def sign_transaction(self, txn: TransactionAPI, **signer_options) -> Optional[Tr return txn - def dump_to_json(self, indent: int = 4): - model = self.model_dump() - model["address"] = self.address - return dumps(model, indent=indent) diff --git a/ape_aws/client.py b/ape_aws/client.py index 30154db..b90f218 100644 --- a/ape_aws/client.py +++ b/ape_aws/client.py @@ -1,4 +1,5 @@ from datetime import datetime +from pathlib import Path from typing import ClassVar import boto3 # type: ignore[import] @@ -79,13 +80,11 @@ class ImportKeyRequest(CreateKeyModel): class ImportKey(ImportKeyRequest): key_id: str = Field(default=None, alias="KeyId") public_key: bytes = Field(default=None, alias="PublicKey") - private_key: str | bytes | None = Field(default=None, alias="PrivateKey") + private_key: str | bytes = Field(default=None, alias="PrivateKey") import_token: bytes = Field(default=None, alias="ImportToken") @field_validator("private_key") def validate_private_key(cls, value): - if not value: - return ec.generate_private_key(ec.SECP256K1(), default_backend()) if value.startswith("0x"): value = value[2:] return value diff --git a/ape_aws/kms/_cli.py b/ape_aws/kms/_cli.py index 86c152f..331f45b 100644 --- a/ape_aws/kms/_cli.py +++ b/ape_aws/kms/_cli.py @@ -1,4 +1,6 @@ import click +from pathlib import Path + from ape.cli import ape_cli_context from ape_aws.accounts import AwsAccountContainer, KmsAccount @@ -62,13 +64,6 @@ def create_key( @kms.command(name="import") @ape_cli_context() -@click.option( - "-p", - "--private-key", - "private_key", - multiple=False, - help="The private key to import", -) @click.option( "-a", "--admin", @@ -93,22 +88,20 @@ def create_key( metavar="str", ) @click.argument("alias_name") +@click.argument("private_key") def import_key( cli_ctx, alias_name: str, - private_key: bytes, + private_key: bytes | str | Path, administrators: list[str], users: list[str], description: str, ): - def ask_for_passphrase(): - return click.prompt( - "Create Passphrase to encrypt account", - hide_input=True, - confirmation_prompt=True, - ) - - passphrase = ask_for_passphrase() + path = Path(private_key) + if path.exists() and path.is_file(): + cli_ctx.logger.info(f"Reading private key from {path}") + private_key = path.read_text().strip() + key_spec = ImportKeyRequest( alias=alias_name, description=description, # type: ignore @@ -130,8 +123,6 @@ def ask_for_passphrase(): if response["ResponseMetadata"]["HTTPStatusCode"] != 200: cli_ctx.abort("Key failed to import into KMS") cli_ctx.logger.success(f"Key imported successfully with ID: {key_id}") - aws_account_container = AwsAccountContainer(name="aws", account_type=KmsAccount) - aws_account_container.add_private_key(alias_name, passphrase, import_key_spec.private_key_hex) # TODO: Add `ape aws kms sign-message [message]` @@ -141,9 +132,8 @@ def ask_for_passphrase(): @kms.command(name="delete") @ape_cli_context() @click.argument("alias_name") -@click.option("-p", "--purge", is_flag=True, help="Purge the key from the system") @click.option("-d", "--days", default=30, help="Number of days until key is deactivated") -def schedule_delete_key(cli_ctx, alias_name, purge, days): +def schedule_delete_key(cli_ctx, alias_name, days): if "alias" not in alias_name: alias_name = f"alias/{alias_name}" kms_account = None @@ -156,7 +146,4 @@ def schedule_delete_key(cli_ctx, alias_name, purge, days): delete_key_spec = DeleteKey(alias=alias_name, key_id=kms_account.key_id, days=days) key_alias = kms_client.delete_key(delete_key_spec) - if purge: - aws_account_container = AwsAccountContainer(name="aws", account_type=KmsAccount) - aws_account_container.delete_account(key_alias) cli_ctx.logger.success(f"Key {key_alias} scheduled for deletion in {days} days") From d81608891cb9bb522bc36c00a1677da60b14e78c Mon Sep 17 00:00:00 2001 From: johnson2427 Date: Fri, 28 Jun 2024 09:55:04 -0500 Subject: [PATCH 15/28] feat: add ability to use mnemonic --- ape_aws/kms/_cli.py | 43 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/ape_aws/kms/_cli.py b/ape_aws/kms/_cli.py index 331f45b..6a43868 100644 --- a/ape_aws/kms/_cli.py +++ b/ape_aws/kms/_cli.py @@ -1,9 +1,11 @@ import click from pathlib import Path +from eth_account import Account as EthAccount +from eth_account.hdaccount import ETHEREUM_DEFAULT_PATH + from ape.cli import ape_cli_context -from ape_aws.accounts import AwsAccountContainer, KmsAccount from ape_aws.client import CreateKey, DeleteKey, ImportKey, ImportKeyRequest, kms_client @@ -64,6 +66,13 @@ def create_key( @kms.command(name="import") @ape_cli_context() +@click.option( + "-p", + "--private-key", + "private_key", + help="The private key you intend to import", + metavar="str", +) @click.option( "-a", "--admin", @@ -87,8 +96,19 @@ def create_key( help="The description of the key you intend to create.", metavar="str", ) +@click.option( + "--use-mnemonic", + "import_from_mnemonic", + help="Import a key from a mnemonic phrase", + is_flag=True, +) +@click.option( + "--hd-path", + "hd_path", + help="The hierarchical deterministic path to derive the key from", + metavar="str", +) @click.argument("alias_name") -@click.argument("private_key") def import_key( cli_ctx, alias_name: str, @@ -96,11 +116,22 @@ def import_key( administrators: list[str], users: list[str], description: str, + import_from_mnemonic: bool, + hd_path: str, ): - path = Path(private_key) - if path.exists() and path.is_file(): - cli_ctx.logger.info(f"Reading private key from {path}") - private_key = path.read_text().strip() + if private_key: + path = Path(private_key) + if path.exists() and path.is_file(): + cli_ctx.logger.info(f"Reading private key from {path}") + private_key = path.read_text().strip() + + if import_from_mnemonic: + if not hd_path: + hd_path = ETHEREUM_DEFAULT_PATH + mnemonic = click.prompt("Enter your mnemonic phrase", hide_input=True) + EthAccount.enable_unaudited_hdwallet_features() + account = EthAccount.from_mnemonic(mnemonic, account_path=hd_path) + private_key = account.key.hex() key_spec = ImportKeyRequest( alias=alias_name, From 01b00ff4d76f40a3121afca006101fcf3d985f40 Mon Sep 17 00:00:00 2001 From: johnson2427 Date: Fri, 28 Jun 2024 10:15:50 -0500 Subject: [PATCH 16/28] refactor: remove unused imports and clean up code --- ape_aws/accounts.py | 2 -- ape_aws/client.py | 1 - ape_aws/kms/_cli.py | 21 ++++++++++++++++++--- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/ape_aws/accounts.py b/ape_aws/accounts.py index 13c58be..59ba18e 100644 --- a/ape_aws/accounts.py +++ b/ape_aws/accounts.py @@ -1,5 +1,4 @@ from functools import cached_property -from json import dumps from typing import Any, Iterator, Optional from ape.api.accounts import AccountAPI, AccountContainerAPI, TransactionAPI @@ -107,4 +106,3 @@ def sign_transaction(self, txn: TransactionAPI, **signer_options) -> Optional[Tr ) return txn - diff --git a/ape_aws/client.py b/ape_aws/client.py index b90f218..89a271e 100644 --- a/ape_aws/client.py +++ b/ape_aws/client.py @@ -1,5 +1,4 @@ from datetime import datetime -from pathlib import Path from typing import ClassVar import boto3 # type: ignore[import] diff --git a/ape_aws/kms/_cli.py b/ape_aws/kms/_cli.py index 6a43868..41b4676 100644 --- a/ape_aws/kms/_cli.py +++ b/ape_aws/kms/_cli.py @@ -96,6 +96,18 @@ def create_key( help="The description of the key you intend to create.", metavar="str", ) +@click.option( + "--from-file", + "import_from_file", + help="Import a key from a file", + is_flag=True, +) +@click.option( + "--file-path", + "file_path", + help="The path to the file containing the private key", + metavar="str | Path", +) @click.option( "--use-mnemonic", "import_from_mnemonic", @@ -112,15 +124,18 @@ def create_key( def import_key( cli_ctx, alias_name: str, - private_key: bytes | str | Path, + private_key: bytes | str, administrators: list[str], users: list[str], description: str, + import_from_file: bool, + file_path: str | Path, import_from_mnemonic: bool, hd_path: str, ): - if private_key: - path = Path(private_key) + if import_from_file: + if isinstance(file_path, str): + path = Path(private_key) if path.exists() and path.is_file(): cli_ctx.logger.info(f"Reading private key from {path}") private_key = path.read_text().strip() From 4ef628efa7391b15e1a10ad4c4fd2775ff23c456 Mon Sep 17 00:00:00 2001 From: johnson2427 Date: Fri, 28 Jun 2024 10:50:42 -0500 Subject: [PATCH 17/28] feat: clean up cli interface --- ape_aws/kms/_cli.py | 40 +++++++++++++++------------------------- 1 file changed, 15 insertions(+), 25 deletions(-) diff --git a/ape_aws/kms/_cli.py b/ape_aws/kms/_cli.py index 41b4676..d07a02f 100644 --- a/ape_aws/kms/_cli.py +++ b/ape_aws/kms/_cli.py @@ -69,9 +69,10 @@ def create_key( @click.option( "-p", "--private-key", - "private_key", + "private_key_path", + type=click.Path(), help="The private key you intend to import", - metavar="str", + metavar="Path", ) @click.option( "-a", @@ -96,18 +97,6 @@ def create_key( help="The description of the key you intend to create.", metavar="str", ) -@click.option( - "--from-file", - "import_from_file", - help="Import a key from a file", - is_flag=True, -) -@click.option( - "--file-path", - "file_path", - help="The path to the file containing the private key", - metavar="str | Path", -) @click.option( "--use-mnemonic", "import_from_mnemonic", @@ -124,23 +113,21 @@ def create_key( def import_key( cli_ctx, alias_name: str, - private_key: bytes | str, + private_key_path: Path, administrators: list[str], users: list[str], description: str, - import_from_file: bool, - file_path: str | Path, import_from_mnemonic: bool, hd_path: str, ): - if import_from_file: - if isinstance(file_path, str): - path = Path(private_key) - if path.exists() and path.is_file(): - cli_ctx.logger.info(f"Reading private key from {path}") - private_key = path.read_text().strip() - - if import_from_mnemonic: + if private_key_path: + if isinstance(private_key_path, str): + private_key_path = Path(private_key_path) + if private_key_path.exists() and private_key_path.is_file(): + cli_ctx.logger.info(f"Reading private key from {private_key_path}") + private_key = private_key_path.read_text().strip() + + elif import_from_mnemonic: if not hd_path: hd_path = ETHEREUM_DEFAULT_PATH mnemonic = click.prompt("Enter your mnemonic phrase", hide_input=True) @@ -148,6 +135,9 @@ def import_key( account = EthAccount.from_mnemonic(mnemonic, account_path=hd_path) private_key = account.key.hex() + else: + private_key = input("Enter your private key: ") + key_spec = ImportKeyRequest( alias=alias_name, description=description, # type: ignore From d0031957c1aec1b537da02d26a7c0878f8545b28 Mon Sep 17 00:00:00 2001 From: johnson2427 Date: Fri, 28 Jun 2024 11:32:22 -0500 Subject: [PATCH 18/28] fix: isort issue --- ape_aws/kms/_cli.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ape_aws/kms/_cli.py b/ape_aws/kms/_cli.py index d07a02f..71e7be7 100644 --- a/ape_aws/kms/_cli.py +++ b/ape_aws/kms/_cli.py @@ -1,11 +1,10 @@ -import click from pathlib import Path +import click +from ape.cli import ape_cli_context from eth_account import Account as EthAccount from eth_account.hdaccount import ETHEREUM_DEFAULT_PATH -from ape.cli import ape_cli_context - from ape_aws.client import CreateKey, DeleteKey, ImportKey, ImportKeyRequest, kms_client From 9e26c0ab14de49580bc6fb052ae00a70c1760c6a Mon Sep 17 00:00:00 2001 From: johnson2427 Date: Mon, 1 Jul 2024 08:38:58 -0500 Subject: [PATCH 19/28] fix: use click prompt to hide password --- ape_aws/kms/_cli.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ape_aws/kms/_cli.py b/ape_aws/kms/_cli.py index 71e7be7..2ca26e2 100644 --- a/ape_aws/kms/_cli.py +++ b/ape_aws/kms/_cli.py @@ -119,6 +119,7 @@ def import_key( import_from_mnemonic: bool, hd_path: str, ): + breakpoint() if private_key_path: if isinstance(private_key_path, str): private_key_path = Path(private_key_path) @@ -135,7 +136,7 @@ def import_key( private_key = account.key.hex() else: - private_key = input("Enter your private key: ") + private_key = click.prompt("Enter your private key: ", hide_input=True) key_spec = ImportKeyRequest( alias=alias_name, From 335640cbc91d973ee9226545696b4b175d95e2a4 Mon Sep 17 00:00:00 2001 From: johnson2427 Date: Mon, 1 Jul 2024 09:57:18 -0500 Subject: [PATCH 20/28] fix: remove breakpoint --- ape_aws/kms/_cli.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ape_aws/kms/_cli.py b/ape_aws/kms/_cli.py index 2ca26e2..2d742a1 100644 --- a/ape_aws/kms/_cli.py +++ b/ape_aws/kms/_cli.py @@ -119,7 +119,6 @@ def import_key( import_from_mnemonic: bool, hd_path: str, ): - breakpoint() if private_key_path: if isinstance(private_key_path, str): private_key_path = Path(private_key_path) From 862a5e57a19287996d584695f9616af709b99669 Mon Sep 17 00:00:00 2001 From: johnson2427 Date: Mon, 1 Jul 2024 11:51:11 -0500 Subject: [PATCH 21/28] feat: add docs to README and remove colon from input request --- README.md | 28 ++++++++++++++++++++++++++-- ape_aws/kms/_cli.py | 2 +- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 00f7636..869f773 100644 --- a/README.md +++ b/README.md @@ -42,13 +42,37 @@ ape aws -h To create a new key: ```bash -ape aws kms create 'KeyAlias' 'Description of new key' +ape aws kms create KeyAlias 'Description of new key' ``` To delete this key: ```bash -ape aws kms delete 'KeyAlias' +ape aws kms delete KeyAlias +``` + +To import an existing private key into KMS: + +```bash +$ ape aws kms import KeyAlias +Enter your private key: +SUCCESS (ape-aws): Key imported successfully with ID: +``` + +You can also import a private key from a file (from hex or bytes): + +```bash +$ ape aws kms import KeyAlias --private-key +INFO (ape-aws): Reading private key from +SUCCESS (ape-aws): Key imported successfully with ID: +``` + +You can import using a mnemonic phrase as well: + +```bash +$ ape aws kms import KeyAlias --use-mnemonic +Enter your mnemonic phrase: +SUCCESS (ape-aws): Key imported successfully with ID: ``` ### IPython diff --git a/ape_aws/kms/_cli.py b/ape_aws/kms/_cli.py index 2d742a1..3e89591 100644 --- a/ape_aws/kms/_cli.py +++ b/ape_aws/kms/_cli.py @@ -135,7 +135,7 @@ def import_key( private_key = account.key.hex() else: - private_key = click.prompt("Enter your private key: ", hide_input=True) + private_key = click.prompt("Enter your private key", hide_input=True) key_spec = ImportKeyRequest( alias=alias_name, From b54927419a80a1f3b0d119a7b0f29d9c1d101678 Mon Sep 17 00:00:00 2001 From: johnson2427 <37009091+johnson2427@users.noreply.github.com> Date: Mon, 1 Jul 2024 12:43:02 -0500 Subject: [PATCH 22/28] fix: update README for create Co-authored-by: El De-dog-lo <3859395+fubuloubu@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 869f773..5f10fd0 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ ape aws -h To create a new key: ```bash -ape aws kms create KeyAlias 'Description of new key' +ape aws kms create KeyAlias -d 'Description of new key' ``` To delete this key: From 5cf67b1f53bb94487cb2bf0f87f87c0d0e95802b Mon Sep 17 00:00:00 2001 From: johnson2427 <37009091+johnson2427@users.noreply.github.com> Date: Mon, 1 Jul 2024 12:52:39 -0500 Subject: [PATCH 23/28] fix: update README Co-authored-by: El De-dog-lo <3859395+fubuloubu@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5f10fd0..a01bd15 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ You can also import a private key from a file (from hex or bytes): ```bash $ ape aws kms import KeyAlias --private-key -INFO (ape-aws): Reading private key from +INFO: Reading private key from SUCCESS (ape-aws): Key imported successfully with ID: ``` From a29c4d720bfe4b63b23ccde9666ee9002a0e352e Mon Sep 17 00:00:00 2001 From: johnson2427 <37009091+johnson2427@users.noreply.github.com> Date: Mon, 1 Jul 2024 12:52:53 -0500 Subject: [PATCH 24/28] fix: update README again Co-authored-by: El De-dog-lo <3859395+fubuloubu@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a01bd15..3772f9d 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ You can import using a mnemonic phrase as well: ```bash $ ape aws kms import KeyAlias --use-mnemonic Enter your mnemonic phrase: -SUCCESS (ape-aws): Key imported successfully with ID: +SUCCESS: Key imported successfully with ID: ``` ### IPython From 56357516a85d8549c64849159529b95f99dfca90 Mon Sep 17 00:00:00 2001 From: johnson2427 <37009091+johnson2427@users.noreply.github.com> Date: Mon, 1 Jul 2024 12:53:51 -0500 Subject: [PATCH 25/28] fix: update metavar for admins Co-authored-by: El De-dog-lo <3859395+fubuloubu@users.noreply.github.com> --- ape_aws/kms/_cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ape_aws/kms/_cli.py b/ape_aws/kms/_cli.py index 3e89591..72610c1 100644 --- a/ape_aws/kms/_cli.py +++ b/ape_aws/kms/_cli.py @@ -79,7 +79,7 @@ def create_key( "administrators", multiple=True, help="Apply key policy to a list of administrators if applicable, ex. -a ARN1, -a ARN2", - metavar="list[ARN]", + metavar="ARN", ) @click.option( "-u", From c6b1a2d69670ee97b989ac03655147b48cfcb753 Mon Sep 17 00:00:00 2001 From: johnson2427 Date: Mon, 1 Jul 2024 13:35:31 -0500 Subject: [PATCH 26/28] feat: add some cleanup for import and README --- README.md | 4 ++-- ape_aws/kms/_cli.py | 10 +--------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 3772f9d..3565a1b 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ To import an existing private key into KMS: ```bash $ ape aws kms import KeyAlias Enter your private key: -SUCCESS (ape-aws): Key imported successfully with ID: +SUCCESS: Key imported successfully with ID: ``` You can also import a private key from a file (from hex or bytes): @@ -64,7 +64,7 @@ You can also import a private key from a file (from hex or bytes): ```bash $ ape aws kms import KeyAlias --private-key INFO: Reading private key from -SUCCESS (ape-aws): Key imported successfully with ID: +SUCCESS: Key imported successfully with ID: ``` You can import using a mnemonic phrase as well: diff --git a/ape_aws/kms/_cli.py b/ape_aws/kms/_cli.py index 72610c1..418f84c 100644 --- a/ape_aws/kms/_cli.py +++ b/ape_aws/kms/_cli.py @@ -21,7 +21,6 @@ def kms(): "administrators", multiple=True, help="Apply key policy to a list of administrators if applicable, ex. -a ARN1, -a ARN2", - metavar="list[ARN]", ) @click.option( "-u", @@ -29,7 +28,6 @@ def kms(): "users", multiple=True, help="Apply key policy to a list of users if applicable, ex. -u ARN1, -u ARN2", - metavar="list[ARN]", ) @click.option( "-d", @@ -71,7 +69,6 @@ def create_key( "private_key_path", type=click.Path(), help="The private key you intend to import", - metavar="Path", ) @click.option( "-a", @@ -79,7 +76,6 @@ def create_key( "administrators", multiple=True, help="Apply key policy to a list of administrators if applicable, ex. -a ARN1, -a ARN2", - metavar="ARN", ) @click.option( "-u", @@ -87,14 +83,12 @@ def create_key( "users", multiple=True, help="Apply key policy to a list of users if applicable, ex. -u ARN1, -u ARN2", - metavar="list[ARN]", ) @click.option( "-d", "--description", "description", help="The description of the key you intend to create.", - metavar="str", ) @click.option( "--use-mnemonic", @@ -106,7 +100,7 @@ def create_key( "--hd-path", "hd_path", help="The hierarchical deterministic path to derive the key from", - metavar="str", + default=ETHEREUM_DEFAULT_PATH, ) @click.argument("alias_name") def import_key( @@ -127,8 +121,6 @@ def import_key( private_key = private_key_path.read_text().strip() elif import_from_mnemonic: - if not hd_path: - hd_path = ETHEREUM_DEFAULT_PATH mnemonic = click.prompt("Enter your mnemonic phrase", hide_input=True) EthAccount.enable_unaudited_hdwallet_features() account = EthAccount.from_mnemonic(mnemonic, account_path=hd_path) From 9850fcaac909a3ad0f2f30da48f02573c56557de Mon Sep 17 00:00:00 2001 From: johnson2427 Date: Mon, 1 Jul 2024 14:30:11 -0500 Subject: [PATCH 27/28] fix: add error handling --- ape_aws/kms/_cli.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/ape_aws/kms/_cli.py b/ape_aws/kms/_cli.py index 418f84c..5fd81ec 100644 --- a/ape_aws/kms/_cli.py +++ b/ape_aws/kms/_cli.py @@ -146,10 +146,15 @@ def import_key( private_key=private_key, # type: ignore import_token=import_token, # type: ignore ) - response = kms_client.import_key(import_key_spec) - if response["ResponseMetadata"]["HTTPStatusCode"] != 200: - cli_ctx.abort("Key failed to import into KMS") - cli_ctx.logger.success(f"Key imported successfully with ID: {key_id}") + try: + response = kms_client.import_key(import_key_spec) + if response["ResponseMetadata"]["HTTPStatusCode"] != 200: + cli_ctx.abort( + f"Key failed to import into KMS, {response['Error']}" + ) + cli_ctx.logger.success(f"Key imported successfully with ID: {key_id}") + except Exception as e: + cli_ctx.logger.error(f"Key failed to import into KMS: {e}") # TODO: Add `ape aws kms sign-message [message]` From 83cfc8156c467f5d75b81bc1e7fdb0ea46c7b777 Mon Sep 17 00:00:00 2001 From: johnson2427 Date: Mon, 1 Jul 2024 16:09:25 -0500 Subject: [PATCH 28/28] fix: black issue --- ape_aws/kms/_cli.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ape_aws/kms/_cli.py b/ape_aws/kms/_cli.py index 5fd81ec..b3b761e 100644 --- a/ape_aws/kms/_cli.py +++ b/ape_aws/kms/_cli.py @@ -149,9 +149,7 @@ def import_key( try: response = kms_client.import_key(import_key_spec) if response["ResponseMetadata"]["HTTPStatusCode"] != 200: - cli_ctx.abort( - f"Key failed to import into KMS, {response['Error']}" - ) + cli_ctx.abort(f"Key failed to import into KMS, {response['Error']}") cli_ctx.logger.success(f"Key imported successfully with ID: {key_id}") except Exception as e: cli_ctx.logger.error(f"Key failed to import into KMS: {e}")