diff --git a/README.md b/README.md index ad5816c..7863f60 100644 --- a/README.md +++ b/README.md @@ -199,14 +199,14 @@ signature = sign(message, key) - `signature`: The generated signature. -### 7. `build_input_text(plaintext, user_aes_key, sender, contract, func_sig, signing_key)` +### 7. `build_input_text(plaintext, user_aes_key, sender, contract, func_selector, signing_key)` **Purpose:** Builds input text by encrypting the plaintext and signing it. **Usage:** ```python -int_cipher_text, signature = build_input_text(plaintext, user_aes_key, sender, contract, func_sig, signing_key) +int_cipher_text, signature = build_input_text(plaintext, user_aes_key, sender, contract, func_selector, signing_key) ``` **Parameters:** @@ -215,7 +215,7 @@ int_cipher_text, signature = build_input_text(plaintext, user_aes_key, sender, c - `user_aes_key`: The user's AES key. - `sender`: The sender's address. - `contract`: The contract address. -- `func_sig`: The function signature. +- `func_selector`: The function selector. - `signing_key`: The private key used for signing. **Returns:** @@ -223,94 +223,104 @@ int_cipher_text, signature = build_input_text(plaintext, user_aes_key, sender, c - `int_cipher_text`: The integer representation of the ciphertext. - `signature`: The generated signature. -### 8. `generate_rsa_keypair()` +### 8. `build_string_input_text(plaintext, user_aes_key, sender, contract, func_selector, signing_key)` -**Purpose:** Generates an RSA key pair. +**Purpose:** Builds input text by encrypting the plaintext and signing it. **Usage:** ```python -private_key_bytes, public_key_bytes = generate_rsa_keypair() +int_cipher_text, signature = build_string_input_text(plaintext, user_aes_key, sender, contract, func_selector, signing_key) ``` +**Parameters:** + +- `plaintext`: The plaintext message. +- `user_aes_key`: The user's AES key. +- `sender`: The sender's address. +- `contract`: The contract address. +- `func_selector`: The function selector. +- `signing_key`: The private key used for signing. + **Returns:** -- `private_key_bytes`: The serialized private key. -- `public_key_bytes`: The serialized public key. +- `input_text`: A dictionary of the form { "ciphertext": { "value": int[] }, "signature": bytes[] } -### 9. `encrypt_rsa(public_key_bytes, plaintext)` +### 9. `build_address_input_text(plaintext, user_aes_key, sender, contract, func_selector, signing_key)` -**Purpose:** Encrypts plaintext using RSA encryption with a provided public key. +**Purpose:** Builds input text by encrypting the plaintext and signing it. **Usage:** ```python -ciphertext = encrypt_rsa(public_key_bytes, plaintext) +int_cipher_text, signature = build_address_input_text(plaintext, user_aes_key, sender, contract, func_selector, signing_key) ``` **Parameters:** -- `public_key_bytes`: The serialized public key. -- `plaintext`: The plaintext message to be encrypted. +- `plaintext`: The plaintext message. +- `user_aes_key`: The user's AES key. +- `sender`: The sender's address. +- `contract`: The contract address. +- `func_selector`: The function selector. +- `signing_key`: The private key used for signing. **Returns:** -- `ciphertext`: The encrypted message. +- `input_text`: A dictionary of the form { "ciphertext": { "ct1": int, "ct2": int, "ct3": int }, "signature1": bytes, "signature2": bytes, "signature3": bytes } -### 10. `decrypt_rsa(private_key_bytes, ciphertext)` +### 10. `generate_rsa_keypair()` -**Purpose:** Decrypts ciphertext using RSA decryption with a provided private key. +**Purpose:** Generates an RSA key pair. **Usage:** ```python -plaintext = decrypt_rsa(private_key_bytes, ciphertext) +private_key_bytes, public_key_bytes = generate_rsa_keypair() ``` -**Parameters:** - -- `private_key_bytes`: The serialized private key. -- `ciphertext`: The encrypted message to be decrypted. - **Returns:** -- `plaintext`: The decrypted message. +- `private_key_bytes`: The serialized private key. +- `public_key_bytes`: The serialized public key. -### 11. `keccak256(data)` +### 11. `encrypt_rsa(public_key_bytes, plaintext)` -**Purpose:** Computes the Keccak-256 hash of the provided data. +**Purpose:** Encrypts plaintext using RSA encryption with a provided public key. **Usage:** ```python -hash_value = keccak256(data) +ciphertext = encrypt_rsa(public_key_bytes, plaintext) ``` **Parameters:** -- `data`: The data to be hashed. +- `public_key_bytes`: The serialized public key. +- `plaintext`: The plaintext message to be encrypted. **Returns:** -- `hash_value`: The computed hash. +- `ciphertext`: The encrypted message. -### 12. `get_func_sig(function_signature)` +### 12. `decrypt_rsa(private_key_bytes, ciphertext)` -**Purpose:** Computes the function signature hash using Keccak-256. +**Purpose:** Decrypts ciphertext using RSA decryption with a provided private key. **Usage:** ```python -func_sig_hash = get_func_sig(function_signature) +plaintext = decrypt_rsa(private_key_bytes, ciphertext) ``` **Parameters:** -- `function_signature`: The function signature string. +- `private_key_bytes`: The serialized private key. +- `ciphertext`: The encrypted message to be decrypted. **Returns:** -- `func_sig_hash`: The first 4 bytes of the computed hash. +- `plaintext`: The decrypted message. ### 13. `decrypt_uint(ciphertext, user_key)` @@ -343,13 +353,32 @@ plaintext = decrypt_string(ciphertext, user_key) **Parameters:** -- `ciphertext`: The value to be decrypted. +- `ciphertext`: A dictionary of the form { "value": int[] } where each cell holds up to 8 characters (padded at the end with zeroes) encrypted - `userKey`: The user's AES key. **Returns:** - `result`: The decrypted value. +### 15. `decrypt_address(ciphertext, user_key)` + +**Purpose:** Decrypts a value stored in a contract and encrypted using a user key + +**Usage:** + +```python +plaintext = decrypt_string(ciphertext, user_key) +``` + +**Parameters:** + +- `ciphertext`: A dictionary of the form { "ct1": int, "ct2": int, "ct3": int } where each cell holds a portion of the address encrypted +- `userKey`: The user's AES key. + +**Returns:** + +- `result`: The decrypted address. + # Utilities (utils.py) Functions ### 1. `web3_connected(web3)` @@ -620,25 +649,7 @@ valid, gas_estimate = is_gas_units_estimation_valid(web3, tx) - `valid`: Boolean indicating if the gas units are sufficient. - `gas_estimate`: The estimated gas units. -### 16. `get_function_signature(function_abi)` - -**Purpose:** Generates the function signature from the ABI. - -**Usage:** - -```python -func_sig = get_function_signature(function_abi) -``` - -**Parameters:** - -- `function_abi`: The ABI of the function. - -**Returns:** - -- `func_sig`: The function signature. - -### 17. `deploy_contract(contract, kwargs, tx_params)` +### 16. `deploy_contract(contract, kwargs, tx_params)` **Purpose:** Deploys a contract with the given parameters. @@ -658,7 +669,7 @@ tx_receipt = deploy_contract(contract, kwargs, tx_params) - `tx_receipt`: The transaction receipt. -### 18. `exec_func_via_transaction(func, tx_params)` +### 17. `exec_func_via_transaction(func, tx_params)` **Purpose:** Executes a contract function via a transaction. @@ -677,7 +688,7 @@ tx_receipt = exec_func_via_transaction(func, tx_params) - `tx_receipt`: The transaction receipt. -### 19. `sign_and_send_tx(web3, private_key, transaction)` +### 18. `sign_and_send_tx(web3, private_key, transaction)` **Purpose:** Signs and sends a transaction. @@ -697,7 +708,7 @@ tx_receipt = sign_and_send_tx(web3, private_key, transaction) - `tx_receipt`: The transaction receipt. -### 20. `decrypt_value(contract_value, user_key)` +### 19. `decrypt_value(contract_value, user_key)` **Purpose:** Decrypts a value stored in a contract using a user key. diff --git a/coti/crypto_utils.py b/coti/crypto_utils.py index 804d2ce..b3f3ba6 100644 --- a/coti/crypto_utils.py +++ b/coti/crypto_utils.py @@ -1,18 +1,15 @@ -import binascii -from array import array - from Crypto.Cipher import AES -from Crypto.Hash import keccak from Crypto.Random import get_random_bytes from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import padding from cryptography.hazmat.primitives.asymmetric import rsa from eth_keys import keys +from web3 import Web3 block_size = AES.block_size address_size = 20 -func_sig_size = 4 +function_selector_size = 4 ct_size = 32 key_size = 32 @@ -75,14 +72,16 @@ def generate_aes_key(): return key -def sign_input_text(sender, addr, func_sig, ct, key): +def sign_input_text(sender, addr, function_selector, ct, key): + function_selector_bytes = bytes.fromhex(function_selector[2:]) + # Ensure all input sizes are the correct length if len(sender) != address_size: raise ValueError(f"Invalid sender address length: {len(sender)} bytes, must be {address_size} bytes") if len(addr) != address_size: raise ValueError(f"Invalid contract address length: {len(addr)} bytes, must be {address_size} bytes") - if len(func_sig) != func_sig_size: - raise ValueError(f"Invalid signature size: {len(func_sig)} bytes, must be {func_sig_size} bytes") + if len(function_selector_bytes) != function_selector_size: + raise ValueError(f"Invalid signature size: {len(function_selector_bytes)} bytes, must be {function_selector_size} bytes") if len(ct) != ct_size: raise ValueError(f"Invalid ct length: {len(ct)} bytes, must be {ct_size} bytes") # Ensure the key is the correct length @@ -90,7 +89,7 @@ def sign_input_text(sender, addr, func_sig, ct, key): raise ValueError(f"Invalid key length: {len(key)} bytes, must be {key_size} bytes") # Create the message to be signed by appending all inputs - message = sender + addr + func_sig + ct + message = sender + addr + function_selector_bytes + ct return sign(message, key) @@ -103,7 +102,7 @@ def sign(message, key): return signature -def build_input_text(plaintext, user_aes_key, sender, contract, func_sig, signing_key): +def build_input_text(plaintext, user_aes_key, sender, contract, function_selector, signing_key): sender_address_bytes = bytes.fromhex(sender.address[2:]) contract_address_bytes = bytes.fromhex(contract.address[2:]) @@ -114,26 +113,90 @@ def build_input_text(plaintext, user_aes_key, sender, contract, func_sig, signin ciphertext, r = encrypt(user_aes_key, plaintext_bytes) ct = ciphertext + r - # Create the function signature - func_hash = get_func_sig(func_sig) # Sign the message - signature = sign_input_text(sender_address_bytes, contract_address_bytes, func_hash, ct, signing_key) + signature = sign_input_text(sender_address_bytes, contract_address_bytes, function_selector, ct, signing_key) # Convert the ct to an integer int_cipher_text = int.from_bytes(ct, byteorder='big') - return int_cipher_text, signature + return { + 'ciphertext': int_cipher_text, + 'signature': signature + } + + +def build_string_input_text(plaintext, user_aes_key, sender, contract, function_selector, signing_key): + input_text = { + 'ciphertext': { + 'value': [] + }, + 'signature': [] + } + encoded_plaintext = bytearray(list(plaintext.encode('utf-8'))) -def build_string_input_text(plaintext, user_aes_key, sender, contract, func_sig, signing_key): - encoded_plaintext = array('B', plaintext.encode('utf-8')) - encrypted_str = [{'ciphertext': 0, 'signature': b''} for _ in range(len(encoded_plaintext))] - for i in range(len(encoded_plaintext)): - ct_int, signature = build_input_text(int(encoded_plaintext[i]), user_aes_key, sender, contract, - func_sig, signing_key) - encrypted_str[i] = {'ciphertext': ct_int, 'signature': signature} + for start_idx in range(0, len(encoded_plaintext), 8): + end_idx = min(start_idx + 8, len(encoded_plaintext)) - return encrypted_str + byte_arr = encoded_plaintext[start_idx:end_idx] + bytearray(8 - (end_idx - start_idx)) + + it_int = build_input_text( + int.from_bytes(byte_arr, 'big'), + user_aes_key, + sender, + contract, + function_selector, + signing_key + ) + + input_text['ciphertext']['value'].append(it_int['ciphertext']) + input_text['signature'].append(it_int['signature']) + + return input_text + +def build_address_input_text(plaintext, user_aes_key, sender, contract, function_selector, signing_key): + if not Web3.is_address(plaintext): + raise ValueError("Plaintext must be a valid address.") + + it_int_1 = build_input_text( + int(plaintext[2:18], 16), # bytes 1 - 8 + user_aes_key, + sender, + contract, + function_selector, + signing_key + ) + + it_int_2 = build_input_text( + int(plaintext[18:34], 16), # bytes 9 - 16 + user_aes_key, + sender, + contract, + function_selector, + signing_key + ) + + it_int_3 = build_input_text( + int(plaintext[34:42], 16), # bytes 17 - 20 + user_aes_key, + sender, + contract, + function_selector, + signing_key + ) + + input_text = { + 'ciphertext': { + 'ct1': it_int_1['ciphertext'], + 'ct2': it_int_2['ciphertext'], + 'ct3': it_int_3['ciphertext'] + }, + 'signature1': it_int_1['signature'], + 'signature2': it_int_2['signature'], + 'signature3': it_int_3['signature'] + } + + return input_text def decrypt_uint(ciphertext, user_key): @@ -154,18 +217,49 @@ def decrypt_uint(ciphertext, user_key): def decrypt_string(ciphertext, user_key): - string_from_input_tx = "" - for input_text_from_tx in ciphertext: - decrypted_input_from_tx = decrypt_uint(input_text_from_tx, user_key) - byte_length = (decrypted_input_from_tx.bit_length() + 7) // 8 # calculate the byte length + if 'value' in ciphertext or hasattr(ciphertext, 'value'): # format when reading ciphertext from an event + __ciphertext = ciphertext['value'] + elif isinstance(ciphertext, tuple): # format when reading ciphertext from state variable + __ciphertext = ciphertext[0] + else: + raise RuntimeError('Unrecognized ciphertext format') + + decrypted_string = "" + + for value in __ciphertext: + decrypted = decrypt_uint(value, user_key) + + byte_length = (decrypted.bit_length() + 7) // 8 # calculate the byte length # Convert the integer to bytes - decrypted_bytes = decrypted_input_from_tx.to_bytes(byte_length, byteorder='big') + decrypted_bytes = decrypted.to_bytes(byte_length, byteorder='big') # Decode the bytes to a string - string_from_input_tx += decrypted_bytes.decode('utf-8') + decrypted_string += decrypted_bytes.decode('utf-8') + + return decrypted_string.strip('\0') - return string_from_input_tx +def decrypt_address(ciphertext, user_key): + if isinstance(ciphertext, list): # format when reading ciphertext from a state variable + __ciphertext = ciphertext + else: # format when reading ciphertext from an event + __ciphertext = list(ciphertext.values()) + + addr = '0x' + + decrypted = decrypt_uint(__ciphertext[0], user_key) + + addr += hex(decrypted)[2:].rjust(16, '0') # 8 bytes is 16 characters + + decrypted = decrypt_uint(__ciphertext[1], user_key) + + addr += hex(decrypted)[2:].rjust(16, '0') # 8 bytes is 16 characters + + decrypted = decrypt_uint(__ciphertext[2], user_key) + + addr += hex(decrypted)[2:].rjust(8, '0') # 4 bytes is 8 characters + + return Web3.to_checksum_address(addr) def generate_rsa_keypair(): @@ -205,26 +299,3 @@ def decrypt_rsa(private_key_bytes, ciphertext): ) ) return plaintext - - -# Function to compute Keccak-256 hash -def keccak256(data): - # Create Keccak-256 hash object - hash_obj = keccak.new(digest_bits=256) - - # Update hash object with data - hash_obj.update(data) - - # Compute hash and return - return hash_obj.digest() - - -def get_func_sig(function_signature): - # Convert function signature to bytes - function_signature_bytes = function_signature.encode('utf-8') - - # Compute Keccak-256 hash on the function signature - function_signature_bytes_hash = keccak256(function_signature_bytes) - - # Take first 4 bytes of the hash - return function_signature_bytes_hash[:4] diff --git a/coti/utils.py b/coti/utils.py index 932fc3d..6e7149e 100644 --- a/coti/utils.py +++ b/coti/utils.py @@ -111,14 +111,6 @@ def is_gas_units_estimation_valid(web3, tx): return False, estimate_gas -def get_function_signature(function_abi): - # Extract the input types from the ABI - input_types = ','.join([param['type'] for param in function_abi.get('inputs', [])]) - - # Generate the function signature - return f"{function_abi['name']}({input_types})" - - def deploy_contract(contract, kwargs, tx_params): func = contract.constructor(**kwargs) return exec_func_via_transaction(func, tx_params)