{% hint style="info" %} The Secret Network CCL SDK can be forked here. {% endhint %}
The essential parameters required for chacha20poly1305
flow are defined in the following data structure:
EncryptedParams
A data structure that is visible to all network participants and can be transmitted over non-secure channels
/// A data structure that is safe to be visible by all network participants and can be transmited over non-secure channels
struct EncryptedParams {
/// Encrypted payload containging hidden message
pub payload : Binary,
/// Sha256 hash of the payload
pub payload_hash : Binary,
/// Signed base64 digest of the payload_hash being wrapped
/// in an cosmos arbitrary (036) object and rehashed again with sha256
pub payload_signature : Binary,
/// Public key of wallet used for deriving a shared key for chacha20_poly1305
/// Not necessary the same as user's public key
pub user_key : Binary,
/// One-time nonce used for chacha20_poly1305 encryption
pub nonce : Binary,
}
Data meant to be encrypted and stored in the payload field of EncryptedParams
/// Data meant to be encrypted and stored in the payload field of [EncryptedParams]
#[cw_serde]
pub struct EncryptedPayload {
/// bech32 prefix address of a wallet used for signing hash of the payload
pub user_address : String,
/// Public key of a wallet used for signing hash of the payload
pub user_pubkey : Binary,
/// Human readable prefix for the bech32 address on the remote cosmos chain
pub hrp : String,
/// Plaintext message e.g. normal `ExecuteMsg` of your contract
pub msg : Binary,
}
Your contract must define an endpoint where a user can pass all the required fields of the EncryptedParams
. E.g:
pub enum ExecuteMsg {
...
Encrypted {
payload : Binary,
payload_signature : Binary,
payload_hash : Binary,
user_key : Binary,
nonce : Binary,
}
...
}
If you want to define a custom message, rename the fields, or add additional fields, there is a helpful trait WithEncryption
that you can implement. It simply tells the compiler how to extract the essential parameters from your custom message and turn it into EncryptedParams
trait WithEncryption : Serialize + Clone {
fn encrypted(&self) -> EncryptedParams;
fn is_encrypted(&self) -> bool;
}
Implementing the trait for your message will allow you to use other useful methods of the SDK (like handle_encrypted_wrapper
) that significantly simplify the development experience.
Example of the implementation for the ExecuteMsg
is as follows:
impl WithEncryption for ExecuteMsg {
fn encrypted(&self) -> EncryptedParams {
match self.clone() {
ExecuteMsg::Encrypted {
payload,
payload_signature,
payload_hash,
user_key,
nonce,
} => EncryptedParams {
payload,
payload_signature,
payload_hash,
user_key,
nonce
},
_ => panic!("Not encrypted")
}
}
fn is_encrypted(&self) -> bool {
if ExecuteMsg::Encrypted{..} = self {
true
} else {
false
}
}
}
The SDK has multiple data structures that already implement WithEncryption
trait and also use the template engine of Rust to make them easily extendable. Take for example the following message:
pub enum GatewayExecuteMsg<E = Option<Empty>>
where E: JsonSchema
{
ResetEncryptionKey {} ,
Encrypted {
payload : Binary,
payload_signature : Binary,
payload_hash : Binary,
user_key : Binary,
nonce : Binary,
},
Extension {
msg : E
}
}
You can define a new message that extends the GatewayExecuteMsg
by simply providing a new type for the Extension
instead of the default Option<Empty>
like this:
// Degine your custom message
#[cw_serde]
pub enum MyCustomMessage {
HandleFoo {}
HandleBar {}
}
// Extend the GatewayExecuteMsg
pub type MyGatewayExecuteMsg = GatewayExecuteMsg<MyCustomMessage>;
Your extended type in this case is available under MyGatewayExecuteMsg::Extension
variant and you can use it in your contract like this:
/// MyGatewayExecuteMsg
match msg {
...
ResetEncryptionKey => { ... },
MyGatewayExecuteMsg::Extension{msg} => {
/// MyCustomMessage
match msg {
MyCustomMessage::HandleFoo{} => {
// Do something
}
MyCustomMessage::HandleBar{} => {
// Do something
}
}
}
...
}
handle_encrypted_wrapper
The encryption logic, handle_encrypted_wrapper
, is where the encryption magic happens ⭐
You can review the function in the SDK here. It has the following functionality:
- Check if Message is Encrypted:
- If the message is encrypted (
msg.is_encrypted()
), it proceeds with decryption.
- If the message is encrypted (
- Extract Encryption Parameters:
- Retrieves the encryption parameters from the message (
msg.encrypted()
).
- Retrieves the encryption parameters from the message (
- Check Nonce:
- Ensures the nonce has not been used before to prevent replay attacks.
- Load Encryption Wallet:
- Loads the encryption wallet from storage.
- Decrypt Payload:
- Decrypts the payload using the wallet and the provided parameters (
payload
,user_key
, andnonce
).
- Decrypts the payload using the wallet and the provided parameters (
let decrypted = wallet.decrypt_to_payload(
¶ms.payload,
¶ms.user_key,
¶ms.nonce,
)?;
decrypt_to_payload uses chacha20poly1305 algorithm
- Verify Credentials:
- Constructs a
CosmosCredential
from the decrypted data. - Inserts the nonce into storage to mark it as used.
- Verifies the sender using the
verify_arbitrary
function with the credential.
- Deserialize Inner Message:
- Converts the decrypted payload into the original message type
E
. - Ensures the decrypted message is not encrypted (nested encryption is not allowed).
- Return Decrypted Message and Updated Info:
- Returns the decrypted message and updated
MessageInfo
with the verified sender.
chacha20poly1305_decrypt
The following function uses the following types for as the input parameters:
cosmwasm_std::Binary
,std::vec::Vec
.[u8]
- and others that implement
Deref<Target = [u8]>
trait
pub fn chacha20poly1305_decrypt(
ciphertext : &impl Deref<Target = [u8]>,
key : &impl Deref<Target = [u8]>,
nonce : &impl Deref<Target = [u8]>,
) -> StdResult<Vec<u8>> {
...
}
To verify a message that was was signed through a method cosmos arbitrary (036)
message format, you can use the following function:
fn verify_arbitrary<M : Display>(api: &dyn Api, cred: &CosmosCredential<M>) -> StdResult<String>
The method takes in a CosmosCredential
struct as an argument which is a a helpful wrapper over essential required fields required for the verification:
pub struct CosmosCredential<M = String>
where M: Display
{
/// public key matching the respective secret that was used to sign message
pub pubkey : Binary,
/// signed sha256 digest of a message wrapped in arbitary data (036) object
pub signature : Binary,
/// signed inner message before being wrapped with 036
pub message : M,
/// prefix for the bech32 address on remote cosmos chain
pub hrp : String
}
Both CosmosCredential
and EncryptedParams
can be used with String
or base64 encoded Binary
types
To generate a preamble message for the cosmos arbitrary (036)
message format, you can use the following utility function:
fn preamble_msg_arb_036(signer: &str, data: &str) -> String
The function uses a hardcoded JSON string with all the required keys present and sorted.