#![cfg_attr(feature = "grpc", doc = " ```no_run")]
#![cfg_attr(not(feature = "grpc"), doc = " ```ignore")]
use aes_siv::{siv::Aes128Siv, Key, KeyInit};
use async_trait::async_trait;
use hex::ToHex;
use hex_literal::hex;
use nanorand::rand::Rng;
use serde::Serialize;
use x25519_dalek::{PublicKey, StaticSecret};
use super::Error;
type Result<T, E = Error> = std::result::Result<T, E>;
const DEVNET_CHAIN_IDS: [&str; 1] = ["secretdev-1"];
const TESTNET_CHAIN_IDS: [&str; 1] = ["pulsar-3"];
const MAINNET_CHAIN_IDS: [&str; 3] = ["secret-2", "secret-3", "secret-4"];
const TESTNET_IO_PUBKEY: [u8; 32] =
hex!("e2b40597d50457d95290bdee480b8bc3400e9f40c2a5d69c9519f1fee2e24933");
const MAINNET_IO_PUBKEY: [u8; 32] =
hex!("efdfbee583877e6d12c219695030a5bfb72e0a3abdc416655aa4a30c95a4446f");
const HKDF_SALT: [u8; 32] =
hex!("000000000000000000024bead8df69990852c202db0e0097c1a12ea637d7e96d");
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait SecretUtils {
async fn encrypt<M: Serialize + Sync>(
&self,
contract_code_hash: &str,
msg: &M,
) -> Result<Vec<u8>>;
async fn decrypt(&self, nonce: &[u8; 32], ciphertext: &[u8]) -> Result<Vec<u8>>;
async fn get_pubkey(&self) -> [u8; 32];
async fn get_tx_encryption_key(&self, nonce: &[u8; 32]) -> [u8; 32];
}
#[derive(Clone)]
pub struct EnigmaUtils {
seed: [u8; 32],
privkey: StaticSecret,
pubkey: PublicKey,
consensus_io_pubkey: [u8; 32],
}
use std::fmt;
impl fmt::Debug for EnigmaUtils {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("EnigmaUtils")
.field("seed", &self.seed.encode_hex::<String>())
.field("privkey", &"[REDACTED]")
.field("pubkey", &self.pubkey.encode_hex::<String>())
.field(
"consensus_io_pubkey",
&self.consensus_io_pubkey.encode_hex::<String>(),
)
.finish()
}
}
impl EnigmaUtils {
pub fn new(seed: Option<[u8; 32]>, chain_id: &str) -> Result<Self> {
let seed = seed.unwrap_or_else(EnigmaUtils::generate_seed);
let (privkey, pubkey) = EnigmaUtils::generate_x25519_key_pair(&seed);
let consensus_io_pubkey = match chain_id {
chain_id if MAINNET_CHAIN_IDS.contains(&chain_id) => Ok(MAINNET_IO_PUBKEY),
chain_id if TESTNET_CHAIN_IDS.contains(&chain_id) => Ok(TESTNET_IO_PUBKEY),
chain_id if DEVNET_CHAIN_IDS.contains(&chain_id) => Err(Error::DevnetIoKey),
_ => Err(Error::InvalidChainId {
chain_id: chain_id.to_string(),
}),
}?;
Ok(EnigmaUtils {
seed,
privkey,
pubkey,
consensus_io_pubkey,
})
}
pub fn from_io_key(seed: Option<[u8; 32]>, consensus_io_pubkey: [u8; 32]) -> Self {
let seed = seed.unwrap_or_else(EnigmaUtils::generate_seed);
let (privkey, pubkey) = EnigmaUtils::generate_x25519_key_pair(&seed);
EnigmaUtils {
seed,
privkey,
pubkey,
consensus_io_pubkey,
}
}
pub fn encrypt<M: ::serde::Serialize>(
&self,
contract_code_hash: &str,
msg: &M,
) -> Result<SecretMsg> {
if contract_code_hash.len() != 64 {
return Err(Error::InvalidCodeHash);
}
let nonce = Self::generate_nonce();
let tx_encryption_key = self.get_tx_encryption_key(&nonce);
let mut cipher = Aes128Siv::new(&Key::<Aes128Siv>::from(tx_encryption_key));
let msg = serde_json::to_vec(msg)?;
let plaintext = [contract_code_hash.as_bytes(), msg.as_slice()].concat();
let ciphertext = cipher.encrypt([[]], &plaintext).map_err(Error::AesSiv)?;
let msg: SecretMsg = [nonce.as_slice(), self.pubkey.as_bytes(), &ciphertext]
.concat()
.into();
Ok(msg)
}
pub fn decrypt(&self, nonce: &[u8; 32], ciphertext: &[u8]) -> Result<Vec<u8>> {
if ciphertext.is_empty() {
return Err(Error::EmptyCiphertext);
}
let tx_encryption_key = self.get_tx_encryption_key(nonce);
let mut cipher = Aes128Siv::new(&Key::<Aes128Siv>::from(tx_encryption_key));
cipher.decrypt([[]], ciphertext).map_err(Error::AesSiv)
}
pub fn get_seed(&self) -> [u8; 32] {
self.seed
}
pub fn get_pubkey(&self) -> [u8; 32] {
*self.pubkey.as_bytes()
}
fn generate_seed() -> [u8; 32] {
let mut seed = [0u8; 32];
let mut rng = nanorand::rand::ChaCha8::new();
rng.fill_bytes(&mut seed);
seed
}
fn generate_x25519_key_pair(seed: &[u8; 32]) -> (StaticSecret, PublicKey) {
let secret = StaticSecret::from(*seed);
let public = PublicKey::from(&secret);
(secret, public)
}
fn generate_nonce() -> [u8; 32] {
let mut nonce = [0u8; 32];
let mut rng = nanorand::rand::ChaCha8::new();
rng.fill_bytes(&mut nonce);
nonce
}
pub fn get_tx_encryption_key(&self, nonce: &[u8; 32]) -> [u8; 32] {
let secret = &self.privkey;
let public = x25519_dalek::PublicKey::from(self.consensus_io_pubkey);
let shared = secret.diffie_hellman(&public);
let ikm = &[shared.as_bytes(), nonce.as_slice()].concat();
let mut key = [0u8; 32];
hkdf::Hkdf::<sha2::Sha256>::new(Some(&HKDF_SALT), ikm)
.expand(&[], &mut key)
.expect("HKDF expansion error");
key
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl SecretUtils for EnigmaUtils {
async fn encrypt<M: Serialize + Sync>(
&self,
contract_code_hash: &str,
msg: &M,
) -> Result<Vec<u8>> {
self.encrypt(contract_code_hash, msg)
.map(|msg| msg.into_inner())
}
async fn decrypt(&self, nonce: &[u8; 32], ciphertext: &[u8]) -> Result<Vec<u8>> {
self.decrypt(nonce, ciphertext)
}
async fn get_pubkey(&self) -> [u8; 32] {
self.get_pubkey()
}
async fn get_tx_encryption_key(&self, nonce: &[u8; 32]) -> [u8; 32] {
self.get_tx_encryption_key(nonce)
}
}
#[derive(Debug, Clone)]
pub struct SecretMsg(Vec<u8>);
impl SecretMsg {
pub fn nonce(&self) -> [u8; 32] {
let mut array = [0u8; 32];
array.copy_from_slice(&self.0[0..32]);
array
}
pub fn pubkey(&self) -> [u8; 32] {
let mut array = [0u8; 32];
array.copy_from_slice(&self.0[32..64]);
array
}
pub fn ciphertext(&self) -> Vec<u8> {
self.0[64..].to_vec()
}
pub fn into_parts(&self) -> ([u8; 32], [u8; 32], Vec<u8>) {
let mut nonce = [0u8; 32];
nonce.copy_from_slice(&self.0[0..32]);
let mut pubkey = [0u8; 32];
pubkey.copy_from_slice(&self.0[32..64]);
let ciphertext = self.0[64..].to_vec();
(nonce, pubkey, ciphertext)
}
pub fn into_inner(self) -> Vec<u8> {
self.0
}
}
impl From<Vec<u8>> for SecretMsg {
fn from(value: Vec<u8>) -> Self {
SecretMsg(value)
}
}
#[cfg(test)]
mod test {
type Error = Box<dyn std::error::Error>;
type Result<T> = std::result::Result<T, Error>;
use super::*;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
struct Msg {
foo: String,
bar: u32,
}
#[test]
fn enigma_utils() -> Result<()> {
let msg = Msg {
foo: "hello world".to_string(),
bar: 42,
};
let utils = EnigmaUtils::new(None, "secret-4")?;
let code_hash = "9a00ca4ad505e9be7e6e6dddf8d939b7ec7e9ac8e109c8681f10db9cacb36d42";
let encrypted = utils.encrypt(code_hash, &msg)?;
let (nonce, _pubkey, ciphertext) = encrypted.into_parts();
let decrypted_bytes = utils.decrypt(&nonce, &ciphertext)?;
let plaintext = format!("{}{}", code_hash, serde_json::to_string(&msg)?);
let decrypted_msg = String::from_utf8(decrypted_bytes)?;
assert_eq!(plaintext, decrypted_msg);
let seed = utils.get_seed();
let pubkey = utils.get_pubkey();
let utils2 = EnigmaUtils::new(Some(seed), "secret-4")?;
let seed2 = utils2.get_seed();
let pubkey2 = utils2.get_pubkey();
assert_eq!(seed, seed2);
assert_eq!(pubkey, pubkey2);
Ok(())
}
}