Soda Labs
Soda Labs Docs

Encrypt to user

Deliver a stored secret or computation result to its authorized owner, encrypted to that user's key.

In the Bubble system, encrypted data are stored, and users receive a "handle" to access their encrypted data via a smart contract. Often, users need the actual data value rather than just the handle. To obtain this information, users must utilize the EncryptToUser gRPC function to request data from the Bubble system.

The EncryptToUser function securely delivers private computation results or stored secrets from the Bubble system exclusively to the authorized user. The process involves confirming user ownership through signature recovery on the handle, verifying on-chain permissions with the ACL, and requesting the sequencer to "offboard" the data by encrypting it from garbledtext™ to an encryption based on the user key for exclusive user access. This process prevents unauthorized access to sensitive outputs tied to a handle, enforces per-chain policy, and provides MPC evaluator signatures, ensuring the application's trust in the result's origin.

Encrypting data for a user is performed through a gRPC function:

rpc EncryptToUser(EncryptToUserRequest) returns (EncryptToUserResponse);

message EncryptToUserRequest{
    bytes handle = 1;
    int64 chain_id = 2;
    bytes user_signature = 3;
}

message EncryptToUserResponse{
    bytes output = 1;
    repeated bytes mpc_signatures = 2;
    int32 result_code = 3;
}

The following Python code demonstrates how to perform an EncryptToUser RPC request using the Bubble SDK:

  • client: A gRPC client.
  • handle: The handle to the encrypted data that must be re-encrypted using the user's AES key.
  • account: The user account requesting the encryption.
  • chain_id: Used by the user interactor to verify the user's permissions.
  • public_keys: The ECDSA keys of the MPC evaluators used for signing the output.
def get_encrypted_value(client, handle, account, chain_id, public_keys):

    handle_bytes = handle.to_bytes(32, byteorder='big')
    # Sign the handle
    signature = sign(handle_bytes, account.key)

    # Call the gRPC service to get the encrypted balance of this handle
    request = pb.EncryptToUserRequest(
		    handle=handle_bytes,
        chain_id=int(chain_id),
        user_signature=signature
	)
    response = client.EncryptToUser(request)

    logging.info(f"EncryptToUser returned {len(response.output)} bytes")

    if len(response.output) != AES_CIPHERTEXT_SIZE*4:
        raise ValueError(f"Invalid response size: {len(response.output)}")

    if len(response.mpc_signatures) != NUM_EVALUATORS:
        raise ValueError(f"Invalid number of signatures: {len(response.mpc_signatures)}")

    if not validate_signatures(response.mpc_signatures, handle_bytes, response.output, public_keys):
        raise ValueError(f"Invalid signatures: {response.mpc_signatures}")

    return response.output

Bubble supplies scripts that perform encryption to user.

  • Script: The script encrypt_to_user, available in both Python and JavaScript, is designed to facilitate encryption.
  • Inputs:
    • --handle flag:
      • Use this flag followed by a handle whose data needs to be encrypted.
    • --decrypt flag:
      • Use this flag to decrypt the data. If set, the AES key will decrypt the encrypted value.
  • Output: The script will print and return the value
  • Execution: Navigate to the main bubble directory and run the appropriate command depending on the language choice:
python3 -m lib.offchain.encryptToUser.python.encrypt_to_user --handle [handle_value] --decrypt