Contract State Encryption
When a contract is executed on chain the state of the contracts needs to be encrypted so that observers can not see the computation that is initialized. The contract should be able to call certain functions inside the enclave and store the contract state on-chain.
A contract can call 3 different functions:
write_db(field_name, value)
, read_db(field_name)
, and remove_db(field_name)
. It is important that the field_name
remains constant between contract calls.We will go over the different steps associated with the encryption of the contract state.
The
contract_key
is the encryption key for the contract state and is a combination of two values: signer_id || authenticated_contract_key
. Every contract has its own unforgeable encryption key. The concatenation of the values is what makes every unique and this is important for several reasons- 1.Make sure the state of two contracts with the same code is different
- 2.Make sure a malicious node runner won't be able to locally encrypt transactions with it's own encryption key, and then decrypt the resulting state with the fake key
so to reiterate, every contract on Secret Network has its own unique and unforgeable encryption key
contract_key
This process of creating
contract_key
is started when the Secret contract is deployed on-chain. First authentication_key
is generated using HDKF-SHA256 inside the enclave from the following values:consensus_state_ikm
HDK-salt
signer_id
signer_id = sha256(concat(msg_sender, block_height));
authentication_key = hkdf({
salt: hkdf_salt,
info: "contract_key",
ikm: concat(consensus_state_ikm, signer_id),
});
From the
authentication_key
create authenticated_contract_key
by calling the hmac-SHA256 hash function with the contract code_hash
as hashing data.This step makes sure the key is unique for every contracts with different code.
authenticated_contract_key = hmac_sha256({
key: authentication_key,
data: code_hash,
});
Lastly concat the
signer_id
and authenticated_contract_key
to create contract_key
. This step makes it so the key is unforgeable as the key can only be recreated with the current signer_id
contract_key = concat(signer_id, authenticated_contract_key);
Every time a contract execution is called,
contract_key
should be sent to the enclave. In the enclave, the following verification needs to happen to proof a genuine contract_key
signer_id = contract_key.slice(0, 32);
expected_contract_key = contract_key.slice(32, 64);
authentication_key = hkdf({
salt: hkdf_salt,
info: "contract_key",
ikm: concat(consensus_state_ikm, signer_id),
});
calculated_contract_key = hmac_sha256({
key: authentication_key,
data: code_hash,
});
assert(calculated_contract_key == expected_contract_key);
write_db(field_name, value)
encryption_key = hkdf({
salt: hkdf_salt,
ikm: concat(consensus_state_ikm, field_name, contract_key),
});
encrypted_field_name = aes_128_siv_encrypt({
key: encryption_key,
data: field_name,
});
current_state_ciphertext = internal_read_db(encrypted_field_name);
if (current_state_ciphertext == null) {
// field_name doesn't yet initialized in state
ad = sha256(encrypted_field_name);
} else {
// read previous_ad, verify it, calculate new ad
previous_ad = current_state_ciphertext.slice(0, 32); // first 32 bytes/256 bits
current_state_ciphertext = current_state_ciphertext.slice(32); // skip first 32 bytes
aes_128_siv_decrypt({
key: encryption_key,
data: current_state_ciphertext,
ad: previous_ad,
}); // just to authenticate previous_ad
ad = sha256(previous_ad);
}
new_state_ciphertext = aes_128_siv_encrypt({
key: encryption_key,
data: value,
ad: ad,
});
new_state = concat(ad, new_state_ciphertext);
internal_write_db(encrypted_field_name, new_state);
read_db(field_name)
encryption_key = hkdf({
salt: hkdf_salt,
ikm: concat(consensus_state_ikm, field_name, contract_key),
});
encrypted_field_name = aes_128_siv_encrypt({
key: encryption_key,
data: field_name,
});
current_state_ciphertext = internal_read_db(encrypted_field_name);
if (current_state_ciphertext == null) {
// field_name doesn't yet initialized in state
return null;
}
// read ad, verify it
ad = current_state_ciphertext.slice(0, 32); // first 32 bytes/256 bits
current_state_ciphertext = current_state_ciphertext.slice(32); // skip first 32 bytes
current_state_plaintext = aes_128_siv_decrypt({
key: encryption_key,
data: current_state_ciphertext,
ad: ad,
});
return current_state_plaintext;
remove_db(field_name)
Very similar to
read_db
.encryption_key = hkdf({
salt: hkdf_salt,
ikm: concat(consensus_state_ikm, field_name, contract_key),
});
encrypted_field_name = aes_128_siv_encrypt({
key: encryption_key,
data: field_name,
});
internal_remove_db(encrypted_field_name);
Last modified 5mo ago