From 1bf8c5f3f08e75276fa812cb248c6eda5cd3beae Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Wed, 19 Jun 2024 20:59:56 +1200 Subject: [PATCH 01/29] dev snip52 toolkit --- Cargo.toml | 3 +- packages/notification/Cargo.toml | 34 +++++++++++ packages/notification/Readme.md | 5 ++ packages/notification/src/crypto.rs | 59 +++++++++++++++++++ packages/notification/src/funcs.rs | 84 ++++++++++++++++++++++++++++ packages/notification/src/lib.rs | 8 +++ packages/notification/src/structs.rs | 70 +++++++++++++++++++++++ src/lib.rs | 2 + 8 files changed, 264 insertions(+), 1 deletion(-) create mode 100644 packages/notification/Cargo.toml create mode 100644 packages/notification/Readme.md create mode 100644 packages/notification/src/crypto.rs create mode 100644 packages/notification/src/funcs.rs create mode 100644 packages/notification/src/lib.rs create mode 100644 packages/notification/src/structs.rs diff --git a/Cargo.toml b/Cargo.toml index bb94dc0..18e35c2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ snip721 = ["secret-toolkit-snip721", "utils"] storage = ["secret-toolkit-storage", "serialization"] utils = ["secret-toolkit-utils"] viewing-key = ["secret-toolkit-viewing-key"] +notification = ["secret-toolkit-notification"] [dependencies] secret-toolkit-crypto = { version = "0.10.0", path = "packages/crypto", optional = true } @@ -45,7 +46,7 @@ secret-toolkit-snip721 = { version = "0.10.0", path = "packages/snip721", option secret-toolkit-storage = { version = "0.10.0", path = "packages/storage", optional = true } secret-toolkit-utils = { version = "0.10.0", path = "packages/utils", optional = true } secret-toolkit-viewing-key = { version = "0.10.0", path = "packages/viewing_key", optional = true } - +secret-toolkit-notification = { version = "0.10.0", path = "packages/notification", optional = true } [workspace] members = ["packages/*"] diff --git a/packages/notification/Cargo.toml b/packages/notification/Cargo.toml new file mode 100644 index 0000000..1203ccd --- /dev/null +++ b/packages/notification/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "secret-toolkit-notification" +version = "0.10.0" +edition = "2021" +authors = ["darwinzer0","blake-regalia"] +license-file = "../../LICENSE" +repository = "https://github.com/scrtlabs/secret-toolkit" +readme = "Readme.md" +description = "Helper tools for SNIP-52 notifications in Secret Contracts" +categories = ["cryptography::cryptocurrencies", "wasm"] +keywords = ["secret-network", "secret-contracts", "secret-toolkit"] + +[package.metadata.docs.rs] +all-features = true + +[dependencies] +cosmwasm-std = { workspace = true, version = "1.0.0" } +serde = { workspace = true } + +ripemd = { version = "0.1.3", default-features = false } +schemars = { workspace = true } + +minicbor-ser = "0.2.0" +# rand_core = { version = "0.6.4", default-features = false } +# rand_chacha = { version = "0.3.1", default-features = false } +sha2 = "0.10.6" +hkdf = "0.12.3" +chacha20poly1305 = { version = "0.10.1", default-features = false, features = ["alloc", "rand_core"] } +generic-array = "0.14.7" +primitive-types = { version = "0.12.2", default-features = false } + +secret-toolkit-crypto = { version = "0.10.0", path = "../crypto", features = [ + "hash", +] } \ No newline at end of file diff --git a/packages/notification/Readme.md b/packages/notification/Readme.md new file mode 100644 index 0000000..85955e8 --- /dev/null +++ b/packages/notification/Readme.md @@ -0,0 +1,5 @@ +# Secret Contract Development Toolkit - SNIP52 (Private Push Notification) Interface + +⚠️ This package is a sub-package of the `secret-toolkit` package. Please see its crate page for more context. + +These functions are meant to help you easily create notification channels for private push notifications in secret contracts. [SNIP-52 Private Push Notification](https://github.com/SolarRepublic/SNIPs/blob/feat/snip-52/SNIP-52.md) \ No newline at end of file diff --git a/packages/notification/src/crypto.rs b/packages/notification/src/crypto.rs new file mode 100644 index 0000000..2e54ef7 --- /dev/null +++ b/packages/notification/src/crypto.rs @@ -0,0 +1,59 @@ +use chacha20poly1305::{ + aead::{AeadInPlace, KeyInit}, + ChaCha20Poly1305, +}; +use cosmwasm_std::{StdError, StdResult}; +use generic_array::GenericArray; +use hkdf::{hmac::Hmac, Hkdf}; +use sha2::{Sha256, Sha512}; + +// Create alias for HMAC-SHA256 +pub type HmacSha256 = Hmac; + +pub fn cipher_data(key: &[u8], nonce: &[u8], plaintext: &[u8], aad: &[u8]) -> StdResult> { + let cipher = ChaCha20Poly1305::new_from_slice(key) + .map_err(|e| StdError::generic_err(format!("{:?}", e)))?; + let mut buffer: Vec = plaintext.to_vec(); + cipher + .encrypt_in_place(GenericArray::from_slice(nonce), aad, &mut buffer) + .map_err(|e| StdError::generic_err(format!("{:?}", e)))?; + Ok(buffer) +} + +pub fn hkdf_sha_256( + salt: &Option>, + ikm: &[u8], + info: &[u8], + length: usize, +) -> StdResult> { + let hk: Hkdf = Hkdf::::new(salt.as_deref().map(|s| s), ikm); + let mut zero_bytes = vec![0u8; length]; + let mut okm = zero_bytes.as_mut_slice(); + match hk.expand(info, &mut okm) { + Ok(_) => Ok(okm.to_vec()), + Err(e) => { + return Err(StdError::generic_err(format!("{:?}", e))); + } + } +} + +pub fn hkdf_sha_512( + salt: &Option>, + ikm: &[u8], + info: &[u8], + length: usize, +) -> StdResult> { + let hk: Hkdf = Hkdf::::new(salt.as_deref().map(|s| s), ikm); + let mut zero_bytes = vec![0u8; length]; + let mut okm = zero_bytes.as_mut_slice(); + match hk.expand(info, &mut okm) { + Ok(_) => Ok(okm.to_vec()), + Err(e) => { + return Err(StdError::generic_err(format!("{:?}", e))); + } + } +} + +pub fn xor_bytes(vec1: &[u8], vec2: &[u8]) -> Vec { + vec1.iter().zip(vec2.iter()).map(|(&a, &b)| a ^ b).collect() +} diff --git a/packages/notification/src/funcs.rs b/packages/notification/src/funcs.rs new file mode 100644 index 0000000..8ff997b --- /dev/null +++ b/packages/notification/src/funcs.rs @@ -0,0 +1,84 @@ +use cosmwasm_std::{Binary, CanonicalAddr, StdResult}; +use secret_toolkit_crypto::sha_256; +use hkdf::hmac::Mac; +use crate::{cipher_data, hkdf_sha_256, HmacSha256}; + +pub const NOTIFICATION_BLOCK_SIZE: usize = 36; +pub const SEED_LEN: usize = 32; + +/// +/// fn notification_id +/// +/// Returns a notification id for the given address and channel id. +/// +pub fn notification_id(seed: &Binary, channel: &str, tx_hash: &String) -> StdResult { + // compute notification ID for this event + let material = [channel.as_bytes(), ":".as_bytes(), tx_hash.as_bytes()].concat(); + + let mut mac: HmacSha256 = HmacSha256::new_from_slice(seed.0.as_slice()).unwrap(); + mac.update(material.as_slice()); + let result = mac.finalize(); + let code_bytes = result.into_bytes(); + Ok(Binary::from(code_bytes.as_slice())) +} + +/// +/// fn encrypt_notification_data +/// +/// Returns encrypted bytes given plaintext bytes, address, and channel id. +/// +pub fn encrypt_notification_data( + block_height: &u64, + tx_hash: &String, + seed: &Binary, + channel: &str, + plaintext: Vec, +) -> StdResult { + let mut padded_plaintext = plaintext.clone(); + zero_pad(&mut padded_plaintext, NOTIFICATION_BLOCK_SIZE); + + let channel_id_bytes = sha_256(channel.as_bytes())[..12].to_vec(); + let salt_bytes = tx_hash.as_bytes()[..12].to_vec(); + let nonce: Vec = channel_id_bytes + .iter() + .zip(salt_bytes.iter()) + .map(|(&b1, &b2)| b1 ^ b2) + .collect(); + let aad = format!("{}:{}", block_height, tx_hash); + + // encrypt notification data for this event + let tag_ciphertext = cipher_data( + seed.0.as_slice(), + nonce.as_slice(), + padded_plaintext.as_slice(), + aad.as_bytes(), + )?; + + Ok(Binary::from(tag_ciphertext.clone())) +} + +/// get the seed for a secret and given address +pub fn get_seed(addr: &CanonicalAddr, secret: &[u8]) -> StdResult { + let seed = hkdf_sha_256( + &None, + secret, + addr.as_slice(), + SEED_LEN, + )?; + Ok(Binary::from(seed)) +} + +/// Take a Vec and pad it up to a multiple of `block_size`, using 0x00 at the end. +fn zero_pad(message: &mut Vec, block_size: usize) -> &mut Vec { + let len = message.len(); + let surplus = len % block_size; + if surplus == 0 { + return message; + } + + let missing = block_size - surplus; + message.reserve(missing); + message.extend(std::iter::repeat(0x00).take(missing)); + message +} + diff --git a/packages/notification/src/lib.rs b/packages/notification/src/lib.rs new file mode 100644 index 0000000..ebc3c98 --- /dev/null +++ b/packages/notification/src/lib.rs @@ -0,0 +1,8 @@ +#![doc = include_str!("../Readme.md")] + +pub mod structs; +pub mod funcs; +pub mod crypto; +pub use structs::*; +pub use funcs::*; +pub use crypto::*; \ No newline at end of file diff --git a/packages/notification/src/structs.rs b/packages/notification/src/structs.rs new file mode 100644 index 0000000..ab55a33 --- /dev/null +++ b/packages/notification/src/structs.rs @@ -0,0 +1,70 @@ +use cosmwasm_std::{Addr, Api, Binary, Env, StdError, StdResult}; +use serde::{Deserialize, Serialize}; + +use crate::{encrypt_notification_data, get_seed, notification_id}; + +#[derive(Serialize, Debug, Deserialize, Clone)] +#[cfg_attr(test, derive(Eq, PartialEq))] +pub struct Notification { + // target for the notification + pub notification_for: Addr, + // data + pub data: T, +} + +impl Notification { + pub fn new(notification_for: Addr, data: T) -> Self { + Notification { + notification_for, + data, + } + } + + pub fn to_txhash_notification( + &self, + api: &dyn Api, + env: &Env, + secret: &[u8], + ) -> StdResult { + let tx_hash = env.transaction.clone().ok_or(StdError::generic_err("no tx hash found"))?.hash; + let notification_for_raw = api.addr_canonicalize(self.notification_for.as_str())?; + let seed = get_seed(¬ification_for_raw, secret)?; + + // get notification id + let id = notification_id(&seed, self.data.channel_id(), &tx_hash)?; + + // use CBOR to encode the data + let cbor_data = self.data.to_cbor(api)?; + + // encrypt the receiver message + let encrypted_data = + encrypt_notification_data(&env.block.height, &tx_hash, &seed, self.data.channel_id(), cbor_data)?; + + Ok(TxHashNotification { + id, + encrypted_data, + }) + } +} + +pub trait NotificationData { + fn to_cbor(&self, api: &dyn Api) -> StdResult>; + fn channel_id(&self) -> &str; +} + +#[derive(Serialize, Debug, Deserialize, Clone)] +#[cfg_attr(test, derive(Eq, PartialEq))] +pub struct TxHashNotification { + pub id: Binary, + pub encrypted_data: Binary, +} + +impl TxHashNotification { + pub fn id_plaintext(&self) -> String { + format!("snip52:{}", self.id.to_base64()) + } + + pub fn data_plaintext(&self) -> String { + self.encrypted_data.to_base64() + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 66bf181..6e87575 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,3 +18,5 @@ pub use secret_toolkit_storage as storage; pub use secret_toolkit_utils as utils; #[cfg(feature = "viewing-key")] pub use secret_toolkit_viewing_key as viewing_key; +#[cfg(feature = "notification")] +pub use secret_toolkit_notification as notification; From f4d83e7dcabf55832150a0f311b58dfbc949e82b Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Mon, 15 Jul 2024 20:02:21 +1200 Subject: [PATCH 02/29] dev: move hkdf functions to crypto package --- packages/crypto/Cargo.toml | 2 ++ .../src/crypto.rs => crypto/src/hkdf.rs} | 21 +------------------ packages/crypto/src/lib.rs | 5 +++++ packages/notification/Cargo.toml | 4 ++-- packages/notification/src/cipher.rs | 20 ++++++++++++++++++ packages/notification/src/funcs.rs | 4 ++-- packages/notification/src/lib.rs | 4 ++-- 7 files changed, 34 insertions(+), 26 deletions(-) rename packages/{notification/src/crypto.rs => crypto/src/hkdf.rs} (62%) create mode 100644 packages/notification/src/cipher.rs diff --git a/packages/crypto/Cargo.toml b/packages/crypto/Cargo.toml index 4ac4312..b787d7a 100644 --- a/packages/crypto/Cargo.toml +++ b/packages/crypto/Cargo.toml @@ -18,6 +18,7 @@ default = ["hash", "ecc-secp256k1", "rand"] hash = ["sha2"] ecc-secp256k1 = ["secp256k1"] rand = ["hash", "rand_chacha", "rand_core"] +hkdf = ["sha2"] [dependencies] rand_core = { version = "0.6.4", default-features = false, optional = true } @@ -26,6 +27,7 @@ sha2 = { version = "0.10.6", default-features = false, optional = true } secp256k1 = { version = "0.27.0", default-features = false, features = [ "alloc", ], optional = true } +hkdf = "0.12.3" cosmwasm-std = { workspace = true } [dev-dependencies] diff --git a/packages/notification/src/crypto.rs b/packages/crypto/src/hkdf.rs similarity index 62% rename from packages/notification/src/crypto.rs rename to packages/crypto/src/hkdf.rs index 2e54ef7..b733a09 100644 --- a/packages/notification/src/crypto.rs +++ b/packages/crypto/src/hkdf.rs @@ -1,25 +1,10 @@ -use chacha20poly1305::{ - aead::{AeadInPlace, KeyInit}, - ChaCha20Poly1305, -}; use cosmwasm_std::{StdError, StdResult}; -use generic_array::GenericArray; use hkdf::{hmac::Hmac, Hkdf}; use sha2::{Sha256, Sha512}; // Create alias for HMAC-SHA256 pub type HmacSha256 = Hmac; -pub fn cipher_data(key: &[u8], nonce: &[u8], plaintext: &[u8], aad: &[u8]) -> StdResult> { - let cipher = ChaCha20Poly1305::new_from_slice(key) - .map_err(|e| StdError::generic_err(format!("{:?}", e)))?; - let mut buffer: Vec = plaintext.to_vec(); - cipher - .encrypt_in_place(GenericArray::from_slice(nonce), aad, &mut buffer) - .map_err(|e| StdError::generic_err(format!("{:?}", e)))?; - Ok(buffer) -} - pub fn hkdf_sha_256( salt: &Option>, ikm: &[u8], @@ -52,8 +37,4 @@ pub fn hkdf_sha_512( return Err(StdError::generic_err(format!("{:?}", e))); } } -} - -pub fn xor_bytes(vec1: &[u8], vec2: &[u8]) -> Vec { - vec1.iter().zip(vec2.iter()).map(|(&a, &b)| a ^ b).collect() -} +} \ No newline at end of file diff --git a/packages/crypto/src/lib.rs b/packages/crypto/src/lib.rs index 5fe8308..cfbcb89 100644 --- a/packages/crypto/src/lib.rs +++ b/packages/crypto/src/lib.rs @@ -12,3 +12,8 @@ pub use hash::{sha_256, SHA256_HASH_SIZE}; #[cfg(feature = "rand")] pub use rng::ContractPrng; + +#[cfg(feature = "hkdf")] +pub mod hkdf; +#[cfg(feature = "hkdf")] +pub use crate::hkdf::*; diff --git a/packages/notification/Cargo.toml b/packages/notification/Cargo.toml index 1203ccd..16dc3fb 100644 --- a/packages/notification/Cargo.toml +++ b/packages/notification/Cargo.toml @@ -24,11 +24,11 @@ minicbor-ser = "0.2.0" # rand_core = { version = "0.6.4", default-features = false } # rand_chacha = { version = "0.3.1", default-features = false } sha2 = "0.10.6" -hkdf = "0.12.3" chacha20poly1305 = { version = "0.10.1", default-features = false, features = ["alloc", "rand_core"] } generic-array = "0.14.7" +hkdf = "0.12.3" primitive-types = { version = "0.12.2", default-features = false } secret-toolkit-crypto = { version = "0.10.0", path = "../crypto", features = [ - "hash", + "hash", "hkdf" ] } \ No newline at end of file diff --git a/packages/notification/src/cipher.rs b/packages/notification/src/cipher.rs new file mode 100644 index 0000000..1d42dc8 --- /dev/null +++ b/packages/notification/src/cipher.rs @@ -0,0 +1,20 @@ +use chacha20poly1305::{ + aead::{AeadInPlace, KeyInit}, + ChaCha20Poly1305, +}; +use cosmwasm_std::{StdError, StdResult}; +use generic_array::GenericArray; + +pub fn cipher_data(key: &[u8], nonce: &[u8], plaintext: &[u8], aad: &[u8]) -> StdResult> { + let cipher = ChaCha20Poly1305::new_from_slice(key) + .map_err(|e| StdError::generic_err(format!("{:?}", e)))?; + let mut buffer: Vec = plaintext.to_vec(); + cipher + .encrypt_in_place(GenericArray::from_slice(nonce), aad, &mut buffer) + .map_err(|e| StdError::generic_err(format!("{:?}", e)))?; + Ok(buffer) +} + +pub fn xor_bytes(vec1: &[u8], vec2: &[u8]) -> Vec { + vec1.iter().zip(vec2.iter()).map(|(&a, &b)| a ^ b).collect() +} diff --git a/packages/notification/src/funcs.rs b/packages/notification/src/funcs.rs index 8ff997b..cd0ef9d 100644 --- a/packages/notification/src/funcs.rs +++ b/packages/notification/src/funcs.rs @@ -1,7 +1,7 @@ use cosmwasm_std::{Binary, CanonicalAddr, StdResult}; -use secret_toolkit_crypto::sha_256; +use secret_toolkit_crypto::{sha_256, hkdf_sha_256, HmacSha256}; use hkdf::hmac::Mac; -use crate::{cipher_data, hkdf_sha_256, HmacSha256}; +use crate::cipher_data; pub const NOTIFICATION_BLOCK_SIZE: usize = 36; pub const SEED_LEN: usize = 32; diff --git a/packages/notification/src/lib.rs b/packages/notification/src/lib.rs index ebc3c98..3e25990 100644 --- a/packages/notification/src/lib.rs +++ b/packages/notification/src/lib.rs @@ -2,7 +2,7 @@ pub mod structs; pub mod funcs; -pub mod crypto; +pub mod cipher; pub use structs::*; pub use funcs::*; -pub use crypto::*; \ No newline at end of file +pub use cipher::*; \ No newline at end of file From 45ed13a4c2076764c735aac646ac09ae789a86ee Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Wed, 7 Aug 2024 21:34:57 +1200 Subject: [PATCH 03/29] dev: add parameter to override default notification block size --- packages/notification/src/funcs.rs | 9 ++++++--- packages/notification/src/structs.rs | 11 +++++++++-- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/packages/notification/src/funcs.rs b/packages/notification/src/funcs.rs index cd0ef9d..2600dc1 100644 --- a/packages/notification/src/funcs.rs +++ b/packages/notification/src/funcs.rs @@ -3,8 +3,9 @@ use secret_toolkit_crypto::{sha_256, hkdf_sha_256, HmacSha256}; use hkdf::hmac::Mac; use crate::cipher_data; +/// default notification block size in bytes pub const NOTIFICATION_BLOCK_SIZE: usize = 36; -pub const SEED_LEN: usize = 32; +pub const SEED_LEN: usize = 32; // 256 bits /// /// fn notification_id @@ -25,7 +26,8 @@ pub fn notification_id(seed: &Binary, channel: &str, tx_hash: &String) -> StdRes /// /// fn encrypt_notification_data /// -/// Returns encrypted bytes given plaintext bytes, address, and channel id. +/// Returns encrypted bytes given plaintext bytes, address, and channel id. +/// Optionally, can set block size (default 36). /// pub fn encrypt_notification_data( block_height: &u64, @@ -33,9 +35,10 @@ pub fn encrypt_notification_data( seed: &Binary, channel: &str, plaintext: Vec, + block_size: Option, ) -> StdResult { let mut padded_plaintext = plaintext.clone(); - zero_pad(&mut padded_plaintext, NOTIFICATION_BLOCK_SIZE); + zero_pad(&mut padded_plaintext, block_size.unwrap_or(NOTIFICATION_BLOCK_SIZE)); let channel_id_bytes = sha_256(channel.as_bytes())[..12].to_vec(); let salt_bytes = tx_hash.as_bytes()[..12].to_vec(); diff --git a/packages/notification/src/structs.rs b/packages/notification/src/structs.rs index ab55a33..fbacf47 100644 --- a/packages/notification/src/structs.rs +++ b/packages/notification/src/structs.rs @@ -25,6 +25,7 @@ impl Notification { api: &dyn Api, env: &Env, secret: &[u8], + block_size: Option, ) -> StdResult { let tx_hash = env.transaction.clone().ok_or(StdError::generic_err("no tx hash found"))?.hash; let notification_for_raw = api.addr_canonicalize(self.notification_for.as_str())?; @@ -37,8 +38,14 @@ impl Notification { let cbor_data = self.data.to_cbor(api)?; // encrypt the receiver message - let encrypted_data = - encrypt_notification_data(&env.block.height, &tx_hash, &seed, self.data.channel_id(), cbor_data)?; + let encrypted_data = encrypt_notification_data( + &env.block.height, + &tx_hash, + &seed, + self.data.channel_id(), + cbor_data, + block_size, + )?; Ok(TxHashNotification { id, From 8fdfeef7d55e917fdd46da48aa1fae4b64402935 Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Wed, 7 Aug 2024 22:15:07 +1200 Subject: [PATCH 04/29] doc: instructions in README --- packages/notification/Readme.md | 58 ++++++++++++++++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/packages/notification/Readme.md b/packages/notification/Readme.md index 85955e8..e1c9901 100644 --- a/packages/notification/Readme.md +++ b/packages/notification/Readme.md @@ -2,4 +2,60 @@ ⚠️ This package is a sub-package of the `secret-toolkit` package. Please see its crate page for more context. -These functions are meant to help you easily create notification channels for private push notifications in secret contracts. [SNIP-52 Private Push Notification](https://github.com/SolarRepublic/SNIPs/blob/feat/snip-52/SNIP-52.md) \ No newline at end of file +These functions are meant to help you easily create notification channels for private push notifications in secret contracts. [SNIP-52 Private Push Notification](https://github.com/SolarRepublic/SNIPs/blob/feat/snip-52/SNIP-52.md) + +### Implementing a `NotificationData` struct + +Each notification channel will have a specified data format, which is defined by defining a struct that implements the `NotificationData` trait that has two methods: `to_cbor` and `channel_id`. The following example illustrates how you might implement this for a channel called `my_channel` and notification data containing two fields: `address` and `amount`. + +```rust +#[derive(Serialize, Debug, Deserialize, Clone)] +pub struct MyNotificationData { + pub message: String, + pub amount: u128, +} + +impl NotificationData for MyNotificationData { + fn to_cbor(&self, _api: &dyn Api) -> StdResult> { + let my_data = cbor::to_vec(&( + self.message.as_bytes(), + self.amount.to_be_bytes(), + )) + .map_err(|e| StdError::generic_err(format!("{:?}", e)))?; + + Ok(my_data) + } + + fn channel_id(&self) -> &str { + "my_channel" + } +} +``` + +The `api` parameter for `to_cbor` is not used in this example, but is there for cases where you might want to convert an `Addr` to a `CanonicalAddr` before encoding using CBOR. + +### Sending a TxHash notification + +To send a notification to a recipient you then create a new `Notification` passing in the address of the recipient along with the notification data you want to send. The following creates a notification for the above `my_channel` and adds it to the contract `Response` as a plaintext attribute. + +```rust +let note = Notification::new( + recipient, + MyNotificationData { + "hello".to_string(), + 1000_u128, + } +); + +// ... other code + +// add notification to response +Ok(Response::new() + .set_data(to_binary(&ExecuteAnswer::MyMessage { status: Success } )?) + .add_attribute_plaintext( + note.id_plaintext(), + note.data_plaintext(), + ) +) +``` + From de841c1100859abc5659c4dffe96ddebc2d665ab Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Wed, 7 Aug 2024 22:17:55 +1200 Subject: [PATCH 05/29] doc: minor edit --- packages/notification/Readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/notification/Readme.md b/packages/notification/Readme.md index e1c9901..3970d7b 100644 --- a/packages/notification/Readme.md +++ b/packages/notification/Readme.md @@ -2,7 +2,7 @@ ⚠️ This package is a sub-package of the `secret-toolkit` package. Please see its crate page for more context. -These functions are meant to help you easily create notification channels for private push notifications in secret contracts. [SNIP-52 Private Push Notification](https://github.com/SolarRepublic/SNIPs/blob/feat/snip-52/SNIP-52.md) +These functions are meant to help you easily create notification channels for private push notifications in secret contracts (see [SNIP-52 Private Push Notification](https://github.com/SolarRepublic/SNIPs/blob/feat/snip-52/SNIP-52.md)). ### Implementing a `NotificationData` struct From dfb5236587432347e36ec331feb54f0289c87350 Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Wed, 7 Aug 2024 22:18:29 +1200 Subject: [PATCH 06/29] doc: minor edit --- packages/notification/Readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/notification/Readme.md b/packages/notification/Readme.md index 3970d7b..43412ee 100644 --- a/packages/notification/Readme.md +++ b/packages/notification/Readme.md @@ -6,7 +6,7 @@ These functions are meant to help you easily create notification channels for pr ### Implementing a `NotificationData` struct -Each notification channel will have a specified data format, which is defined by defining a struct that implements the `NotificationData` trait that has two methods: `to_cbor` and `channel_id`. The following example illustrates how you might implement this for a channel called `my_channel` and notification data containing two fields: `address` and `amount`. +Each notification channel will have a specified data format, which is defined by creating a struct that implements the `NotificationData` trait that has two methods: `to_cbor` and `channel_id`. The following example illustrates how you might implement this for a channel called `my_channel` and notification data containing two fields: `address` and `amount`. ```rust #[derive(Serialize, Debug, Deserialize, Clone)] From 4413ffecc130a589875afae6705ecd43d23e395e Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Wed, 7 Aug 2024 22:19:30 +1200 Subject: [PATCH 07/29] doc: fix typo --- packages/notification/Readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/notification/Readme.md b/packages/notification/Readme.md index 43412ee..bc19b95 100644 --- a/packages/notification/Readme.md +++ b/packages/notification/Readme.md @@ -6,7 +6,7 @@ These functions are meant to help you easily create notification channels for pr ### Implementing a `NotificationData` struct -Each notification channel will have a specified data format, which is defined by creating a struct that implements the `NotificationData` trait that has two methods: `to_cbor` and `channel_id`. The following example illustrates how you might implement this for a channel called `my_channel` and notification data containing two fields: `address` and `amount`. +Each notification channel will have a specified data format, which is defined by creating a struct that implements the `NotificationData` trait, which has two methods: `to_cbor` and `channel_id`. The following example illustrates how you might implement this for a channel called `my_channel` and notification data containing two fields: `message` and `amount`. ```rust #[derive(Serialize, Debug, Deserialize, Clone)] From d18cf7e58e83dd83b958216d3cdd25c762cb99d9 Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Thu, 8 Aug 2024 15:50:21 +1200 Subject: [PATCH 08/29] docs: txhash notification example --- packages/notification/Readme.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/notification/Readme.md b/packages/notification/Readme.md index bc19b95..4cf0538 100644 --- a/packages/notification/Readme.md +++ b/packages/notification/Readme.md @@ -6,7 +6,9 @@ These functions are meant to help you easily create notification channels for pr ### Implementing a `NotificationData` struct -Each notification channel will have a specified data format, which is defined by creating a struct that implements the `NotificationData` trait, which has two methods: `to_cbor` and `channel_id`. The following example illustrates how you might implement this for a channel called `my_channel` and notification data containing two fields: `message` and `amount`. +Each notification channel will have a specified data format, which is defined by creating a struct that implements the `NotificationData` trait, which has two methods: `to_cbor` and `channel_id`. + +The following example illustrates how you might implement this for a channel called `my_channel` and notification data containing two fields: `message` and `amount`. ```rust #[derive(Serialize, Debug, Deserialize, Clone)] @@ -36,16 +38,19 @@ The `api` parameter for `to_cbor` is not used in this example, but is there for ### Sending a TxHash notification -To send a notification to a recipient you then create a new `Notification` passing in the address of the recipient along with the notification data you want to send. The following creates a notification for the above `my_channel` and adds it to the contract `Response` as a plaintext attribute. +To send a notification to a recipient you first create a new `Notification` struct passing in the address of the recipient along with the notification data you want to send. Then to turn it into a `TxHashNotification` execute the `to_txhash_notification` method on the `Notification` by passing in `deps.api`, `env`, and an internal `secret`, which is a randomly generated byte slice that has been stored previously in your contract during initialization. + +The following code snippet creates a notification for the above `my_channel` and adds it to the contract `Response` as a plaintext attribute. ```rust -let note = Notification::new( +let notification = Notification::new( recipient, MyNotificationData { "hello".to_string(), 1000_u128, } -); +) +.to_txhash_notification(deps.api, &env, secret)?; // ... other code @@ -53,8 +58,8 @@ let note = Notification::new( Ok(Response::new() .set_data(to_binary(&ExecuteAnswer::MyMessage { status: Success } )?) .add_attribute_plaintext( - note.id_plaintext(), - note.data_plaintext(), + notification.id_plaintext(), + notification.data_plaintext(), ) ) ``` From dc54f5accc24badbd5ce5a69af1bfd2df367f22c Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Thu, 8 Aug 2024 19:51:32 +1200 Subject: [PATCH 09/29] minor clippy fixes --- packages/crypto/src/hkdf.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/crypto/src/hkdf.rs b/packages/crypto/src/hkdf.rs index b733a09..583b217 100644 --- a/packages/crypto/src/hkdf.rs +++ b/packages/crypto/src/hkdf.rs @@ -11,13 +11,13 @@ pub fn hkdf_sha_256( info: &[u8], length: usize, ) -> StdResult> { - let hk: Hkdf = Hkdf::::new(salt.as_deref().map(|s| s), ikm); + let hk: Hkdf = Hkdf::::new(salt.as_deref(), ikm); let mut zero_bytes = vec![0u8; length]; - let mut okm = zero_bytes.as_mut_slice(); - match hk.expand(info, &mut okm) { + let okm = zero_bytes.as_mut_slice(); + match hk.expand(info, okm) { Ok(_) => Ok(okm.to_vec()), Err(e) => { - return Err(StdError::generic_err(format!("{:?}", e))); + Err(StdError::generic_err(format!("{:?}", e))) } } } @@ -28,13 +28,13 @@ pub fn hkdf_sha_512( info: &[u8], length: usize, ) -> StdResult> { - let hk: Hkdf = Hkdf::::new(salt.as_deref().map(|s| s), ikm); + let hk: Hkdf = Hkdf::::new(salt.as_deref(), ikm); let mut zero_bytes = vec![0u8; length]; - let mut okm = zero_bytes.as_mut_slice(); - match hk.expand(info, &mut okm) { + let okm = zero_bytes.as_mut_slice(); + match hk.expand(info, okm) { Ok(_) => Ok(okm.to_vec()), Err(e) => { - return Err(StdError::generic_err(format!("{:?}", e))); + Err(StdError::generic_err(format!("{:?}", e))) } } } \ No newline at end of file From 2d1828c62dff834ee9e6167b8d9670a7e8960cd6 Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Thu, 10 Oct 2024 15:57:59 +1300 Subject: [PATCH 10/29] as use statments --- packages/notification/Readme.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/notification/Readme.md b/packages/notification/Readme.md index 4cf0538..4c305c0 100644 --- a/packages/notification/Readme.md +++ b/packages/notification/Readme.md @@ -11,6 +11,11 @@ Each notification channel will have a specified data format, which is defined by The following example illustrates how you might implement this for a channel called `my_channel` and notification data containing two fields: `message` and `amount`. ```rust +use cosmwasm_std::{Api, StdError, StdResult}; +use secret_toolkit::notification::NotificationData; +use serde::{Deserialize, Serialize}; +use minicbor_ser as cbor; + #[derive(Serialize, Debug, Deserialize, Clone)] pub struct MyNotificationData { pub message: String, From c28aed1899c8308f4ee261351ae8a875b2a2699d Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Fri, 11 Oct 2024 08:34:31 +1300 Subject: [PATCH 11/29] add channel info types --- packages/notification/src/structs.rs | 62 +++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/packages/notification/src/structs.rs b/packages/notification/src/structs.rs index fbacf47..69df77e 100644 --- a/packages/notification/src/structs.rs +++ b/packages/notification/src/structs.rs @@ -1,4 +1,5 @@ -use cosmwasm_std::{Addr, Api, Binary, Env, StdError, StdResult}; +use cosmwasm_std::{Addr, Api, Binary, Env, StdError, StdResult, Uint64}; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use crate::{encrypt_notification_data, get_seed, notification_id}; @@ -74,4 +75,63 @@ impl TxHashNotification { pub fn data_plaintext(&self) -> String { self.encrypted_data.to_base64() } +} + +// types for channel info response + +#[derive(Serialize, Deserialize, JsonSchema, Clone, Debug)] +pub struct ChannelInfoData { + /// same as query input + pub channel: String, + /// "counter", "txhash", "bloom" + pub mode: String, + + /// txhash / bloom fields only + /// if txhash argument was given, this will be its computed Notification ID + pub answer_id: Option, + + /// bloom fields only + /// bloom filter parameters + pub parameters: Option, + /// bloom filter data + pub data: Option, + + /// counter fields only + /// current counter value + pub counter: Option, + /// the next Notification ID + pub next_id: Option, + + /// counter / txhash field only + /// optional CDDL schema definition string for the CBOR-encoded notification data + pub cddl: Option, +} + +#[derive(Serialize, Deserialize, JsonSchema, Clone, Debug)] +pub struct BloomParameters { + pub m: u32, + pub k: u32, + pub h: String, +} + +#[derive(Serialize, Deserialize, JsonSchema, Clone, Debug)] +pub struct Descriptor { + pub r#type: String, + pub version: String, + pub packet_size: u32, + pub data: StructDescriptor, +} + +#[derive(Serialize, Deserialize, JsonSchema, Clone, Debug)] +pub struct StructDescriptor { + pub r#type: String, + pub label: String, + pub members: Vec, +} + +#[derive(Serialize, Deserialize, JsonSchema, Clone, Debug)] +pub struct FlatDescriptor { + pub r#type: String, + pub label: String, + pub description: Option, } \ No newline at end of file From 0c6a362115ed8b6f9dc01b624945436174ca5a18 Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Fri, 11 Oct 2024 09:15:09 +1300 Subject: [PATCH 12/29] add id to string func --- packages/notification/src/structs.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/notification/src/structs.rs b/packages/notification/src/structs.rs index 69df77e..d644165 100644 --- a/packages/notification/src/structs.rs +++ b/packages/notification/src/structs.rs @@ -58,6 +58,7 @@ impl Notification { pub trait NotificationData { fn to_cbor(&self, api: &dyn Api) -> StdResult>; fn channel_id(&self) -> &str; + fn id_to_string() -> String; } #[derive(Serialize, Debug, Deserialize, Clone)] From f01d675c95a8b1c88eabae9ea7d3a6360e497f59 Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Fri, 11 Oct 2024 09:33:25 +1300 Subject: [PATCH 13/29] make id a const in notification data --- packages/notification/src/structs.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/notification/src/structs.rs b/packages/notification/src/structs.rs index d644165..e3501a5 100644 --- a/packages/notification/src/structs.rs +++ b/packages/notification/src/structs.rs @@ -33,7 +33,7 @@ impl Notification { let seed = get_seed(¬ification_for_raw, secret)?; // get notification id - let id = notification_id(&seed, self.data.channel_id(), &tx_hash)?; + let id = notification_id(&seed, self.data.channel_id().as_str(), &tx_hash)?; // use CBOR to encode the data let cbor_data = self.data.to_cbor(api)?; @@ -43,7 +43,7 @@ impl Notification { &env.block.height, &tx_hash, &seed, - self.data.channel_id(), + self.data.channel_id().as_str(), cbor_data, block_size, )?; @@ -56,9 +56,11 @@ impl Notification { } pub trait NotificationData { + const CHANNEL_ID: String; fn to_cbor(&self, api: &dyn Api) -> StdResult>; - fn channel_id(&self) -> &str; - fn id_to_string() -> String; + fn channel_id(&self) -> String { + Self::CHANNEL_ID + } } #[derive(Serialize, Debug, Deserialize, Clone)] From 746147b375bd9cbd0be5172965c57a162e8e4b1e Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Fri, 11 Oct 2024 09:36:32 +1300 Subject: [PATCH 14/29] make channel id static str --- packages/notification/src/structs.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/notification/src/structs.rs b/packages/notification/src/structs.rs index e3501a5..d1fce20 100644 --- a/packages/notification/src/structs.rs +++ b/packages/notification/src/structs.rs @@ -56,10 +56,10 @@ impl Notification { } pub trait NotificationData { - const CHANNEL_ID: String; + const CHANNEL_ID: &'static str; fn to_cbor(&self, api: &dyn Api) -> StdResult>; fn channel_id(&self) -> String { - Self::CHANNEL_ID + Self::CHANNEL_ID.to_string() } } From 8aed92d589dc119f69d20f8538d5a6eea8003d95 Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Fri, 11 Oct 2024 10:14:05 +1300 Subject: [PATCH 15/29] add cddl to notificationdata struct --- packages/notification/src/structs.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/notification/src/structs.rs b/packages/notification/src/structs.rs index d1fce20..980dbd0 100644 --- a/packages/notification/src/structs.rs +++ b/packages/notification/src/structs.rs @@ -57,10 +57,14 @@ impl Notification { pub trait NotificationData { const CHANNEL_ID: &'static str; + const CDDL_SCHEMA: &'static str; fn to_cbor(&self, api: &dyn Api) -> StdResult>; fn channel_id(&self) -> String { Self::CHANNEL_ID.to_string() } + fn cddl_schema(&self) -> String { + Self::CDDL_SCHEMA.to_string() + } } #[derive(Serialize, Debug, Deserialize, Clone)] From 73c2ff9320b8186a652f5247f446ca526b164b27 Mon Sep 17 00:00:00 2001 From: Blake Regalia Date: Tue, 26 Nov 2024 10:34:57 -0800 Subject: [PATCH 16/29] fix: uppercase txhash --- packages/notification/src/funcs.rs | 4 ++-- packages/notification/src/structs.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/notification/src/funcs.rs b/packages/notification/src/funcs.rs index 2600dc1..09d194b 100644 --- a/packages/notification/src/funcs.rs +++ b/packages/notification/src/funcs.rs @@ -14,7 +14,7 @@ pub const SEED_LEN: usize = 32; // 256 bits /// pub fn notification_id(seed: &Binary, channel: &str, tx_hash: &String) -> StdResult { // compute notification ID for this event - let material = [channel.as_bytes(), ":".as_bytes(), tx_hash.as_bytes()].concat(); + let material = [channel.as_bytes(), ":".as_bytes(), tx_hash.to_ascii_uppercase().as_bytes()].concat(); let mut mac: HmacSha256 = HmacSha256::new_from_slice(seed.0.as_slice()).unwrap(); mac.update(material.as_slice()); @@ -41,7 +41,7 @@ pub fn encrypt_notification_data( zero_pad(&mut padded_plaintext, block_size.unwrap_or(NOTIFICATION_BLOCK_SIZE)); let channel_id_bytes = sha_256(channel.as_bytes())[..12].to_vec(); - let salt_bytes = tx_hash.as_bytes()[..12].to_vec(); + let salt_bytes = tx_hash.to_ascii_uppercase().as_bytes()[..12].to_vec(); let nonce: Vec = channel_id_bytes .iter() .zip(salt_bytes.iter()) diff --git a/packages/notification/src/structs.rs b/packages/notification/src/structs.rs index 980dbd0..888ec5a 100644 --- a/packages/notification/src/structs.rs +++ b/packages/notification/src/structs.rs @@ -28,7 +28,7 @@ impl Notification { secret: &[u8], block_size: Option, ) -> StdResult { - let tx_hash = env.transaction.clone().ok_or(StdError::generic_err("no tx hash found"))?.hash; + let tx_hash = env.transaction.clone().ok_or(StdError::generic_err("no tx hash found"))?.hash.to_ascii_uppercase(); let notification_for_raw = api.addr_canonicalize(self.notification_for.as_str())?; let seed = get_seed(¬ification_for_raw, secret)?; From 0aea1873800839b955be6dfa09591063c70b35a3 Mon Sep 17 00:00:00 2001 From: Blake Regalia Date: Tue, 26 Nov 2024 13:08:14 -0800 Subject: [PATCH 17/29] fix: hex decode txhash for salt --- packages/notification/Cargo.toml | 1 + packages/notification/src/funcs.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/notification/Cargo.toml b/packages/notification/Cargo.toml index 16dc3fb..040f77a 100644 --- a/packages/notification/Cargo.toml +++ b/packages/notification/Cargo.toml @@ -28,6 +28,7 @@ chacha20poly1305 = { version = "0.10.1", default-features = false, features = [" generic-array = "0.14.7" hkdf = "0.12.3" primitive-types = { version = "0.12.2", default-features = false } +hex = "0.4.3" secret-toolkit-crypto = { version = "0.10.0", path = "../crypto", features = [ "hash", "hkdf" diff --git a/packages/notification/src/funcs.rs b/packages/notification/src/funcs.rs index 09d194b..b14bdc2 100644 --- a/packages/notification/src/funcs.rs +++ b/packages/notification/src/funcs.rs @@ -41,7 +41,7 @@ pub fn encrypt_notification_data( zero_pad(&mut padded_plaintext, block_size.unwrap_or(NOTIFICATION_BLOCK_SIZE)); let channel_id_bytes = sha_256(channel.as_bytes())[..12].to_vec(); - let salt_bytes = tx_hash.to_ascii_uppercase().as_bytes()[..12].to_vec(); + let salt_bytes = hex::decode(tx_hash).unwrap().as_bytes()[..12].to_vec(); let nonce: Vec = channel_id_bytes .iter() .zip(salt_bytes.iter()) From 1c60850fd3e41d0aa0320d7f93802db13ce4c134 Mon Sep 17 00:00:00 2001 From: Blake Regalia Date: Tue, 26 Nov 2024 13:12:08 -0800 Subject: [PATCH 18/29] fix: bytes --- packages/notification/src/funcs.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/notification/src/funcs.rs b/packages/notification/src/funcs.rs index b14bdc2..621af37 100644 --- a/packages/notification/src/funcs.rs +++ b/packages/notification/src/funcs.rs @@ -41,7 +41,7 @@ pub fn encrypt_notification_data( zero_pad(&mut padded_plaintext, block_size.unwrap_or(NOTIFICATION_BLOCK_SIZE)); let channel_id_bytes = sha_256(channel.as_bytes())[..12].to_vec(); - let salt_bytes = hex::decode(tx_hash).unwrap().as_bytes()[..12].to_vec(); + let salt_bytes = hex::decode(tx_hash).unwrap()[..12].to_vec(); let nonce: Vec = channel_id_bytes .iter() .zip(salt_bytes.iter()) From 302510d616b53c7281a471db085404da4f4eba84 Mon Sep 17 00:00:00 2001 From: Blake Regalia Date: Tue, 26 Nov 2024 22:13:00 -0800 Subject: [PATCH 19/29] feat: data block size --- packages/notification/src/structs.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/notification/src/structs.rs b/packages/notification/src/structs.rs index 888ec5a..65ef3d3 100644 --- a/packages/notification/src/structs.rs +++ b/packages/notification/src/structs.rs @@ -58,6 +58,7 @@ impl Notification { pub trait NotificationData { const CHANNEL_ID: &'static str; const CDDL_SCHEMA: &'static str; + const BLOCK_SIZE: usize; fn to_cbor(&self, api: &dyn Api) -> StdResult>; fn channel_id(&self) -> String { Self::CHANNEL_ID.to_string() From 61e1f363b97e42d715ae111ebacdc86d90bd8680 Mon Sep 17 00:00:00 2001 From: Blake Regalia Date: Wed, 27 Nov 2024 08:05:31 -0800 Subject: [PATCH 20/29] feat: payload encoding --- packages/notification/src/structs.rs | 65 +++++++++++++++++++++------- 1 file changed, 49 insertions(+), 16 deletions(-) diff --git a/packages/notification/src/structs.rs b/packages/notification/src/structs.rs index 65ef3d3..478269a 100644 --- a/packages/notification/src/structs.rs +++ b/packages/notification/src/structs.rs @@ -7,12 +7,49 @@ use crate::{encrypt_notification_data, get_seed, notification_id}; #[derive(Serialize, Debug, Deserialize, Clone)] #[cfg_attr(test, derive(Eq, PartialEq))] pub struct Notification { - // target for the notification + /// Recipient address of the notification pub notification_for: Addr, - // data + /// Typed notification data pub data: T, } + +pub trait NotificationData { + const CHANNEL_ID: &'static str; + const CDDL_SCHEMA: &'static str; + const ELEMENTS: u64; + const PAYLOAD_SIZE: usize; + + fn channel_id(&self) -> String { + Self::CHANNEL_ID.to_string() + } + + fn cddl_schema(&self) -> String { + Self::CDDL_SCHEMA.to_string() + } + + fn to_cbor(&self, api: &dyn Api) -> StdResult> { + // dynamically allocate output buffer + let mut buffer = vec![0u8; Self::PAYLOAD_SIZE]; + + // create CBOR encoder + let mut encoder = Encoder::new(&mut buffer[..]); + + // encode number of elements + encoder.array(Self::ELEMENTS).map_err(cbor_to_std_error)?; + + // encode CBOR data + self.encode_cbor(api, &mut encoder)?; + + // return buffer + Ok(buffer) + } + + /// CBOR encodes notification data into the encoder + fn encode_cbor(&self, api: &dyn Api, encoder: &mut Encoder<&mut [u8]>) -> StdResult<()>; +} + + impl Notification { pub fn new(notification_for: Addr, data: T) -> Self { Notification { @@ -28,11 +65,18 @@ impl Notification { secret: &[u8], block_size: Option, ) -> StdResult { - let tx_hash = env.transaction.clone().ok_or(StdError::generic_err("no tx hash found"))?.hash.to_ascii_uppercase(); + // extract and normalize tx hash + let tx_hash = env.transaction.clone() + .ok_or(StdError::generic_err("no tx hash found"))? + .hash.to_ascii_uppercase(); + + // canonicalize notification recipient address let notification_for_raw = api.addr_canonicalize(self.notification_for.as_str())?; + + // derive recipient's notification seed let seed = get_seed(¬ification_for_raw, secret)?; - // get notification id + // derive notification id let id = notification_id(&seed, self.data.channel_id().as_str(), &tx_hash)?; // use CBOR to encode the data @@ -48,6 +92,7 @@ impl Notification { block_size, )?; + // enstruct Ok(TxHashNotification { id, encrypted_data, @@ -55,18 +100,6 @@ impl Notification { } } -pub trait NotificationData { - const CHANNEL_ID: &'static str; - const CDDL_SCHEMA: &'static str; - const BLOCK_SIZE: usize; - fn to_cbor(&self, api: &dyn Api) -> StdResult>; - fn channel_id(&self) -> String { - Self::CHANNEL_ID.to_string() - } - fn cddl_schema(&self) -> String { - Self::CDDL_SCHEMA.to_string() - } -} #[derive(Serialize, Debug, Deserialize, Clone)] #[cfg_attr(test, derive(Eq, PartialEq))] From e76c64e89178c13f34cf17cc20aff8f36fcac5e4 Mon Sep 17 00:00:00 2001 From: Blake Regalia Date: Wed, 27 Nov 2024 08:17:03 -0800 Subject: [PATCH 21/29] fix: dependencies --- packages/notification/Cargo.toml | 2 +- packages/notification/src/structs.rs | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/notification/Cargo.toml b/packages/notification/Cargo.toml index 040f77a..cf4bb87 100644 --- a/packages/notification/Cargo.toml +++ b/packages/notification/Cargo.toml @@ -20,7 +20,6 @@ serde = { workspace = true } ripemd = { version = "0.1.3", default-features = false } schemars = { workspace = true } -minicbor-ser = "0.2.0" # rand_core = { version = "0.6.4", default-features = false } # rand_chacha = { version = "0.3.1", default-features = false } sha2 = "0.10.6" @@ -29,6 +28,7 @@ generic-array = "0.14.7" hkdf = "0.12.3" primitive-types = { version = "0.12.2", default-features = false } hex = "0.4.3" +minicbor = "0.25.1" secret-toolkit-crypto = { version = "0.10.0", path = "../crypto", features = [ "hash", "hkdf" diff --git a/packages/notification/src/structs.rs b/packages/notification/src/structs.rs index 478269a..95c68a0 100644 --- a/packages/notification/src/structs.rs +++ b/packages/notification/src/structs.rs @@ -1,6 +1,7 @@ use cosmwasm_std::{Addr, Api, Binary, Env, StdError, StdResult, Uint64}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use minicbor_ser::{encode as cbor_encode, Encoder}; use crate::{encrypt_notification_data, get_seed, notification_id}; @@ -14,6 +15,10 @@ pub struct Notification { } +pub fn cbor_to_std_error(e: cbor_encode::Error) -> StdError { + StdError::generic_err("CBOR encoding error") +} + pub trait NotificationData { const CHANNEL_ID: &'static str; const CDDL_SCHEMA: &'static str; From a03d708d2162fb31b0548548fb1be707881a9cb5 Mon Sep 17 00:00:00 2001 From: Blake Regalia Date: Wed, 27 Nov 2024 08:18:40 -0800 Subject: [PATCH 22/29] fix: import --- packages/notification/src/structs.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/notification/src/structs.rs b/packages/notification/src/structs.rs index 95c68a0..216948a 100644 --- a/packages/notification/src/structs.rs +++ b/packages/notification/src/structs.rs @@ -1,7 +1,7 @@ use cosmwasm_std::{Addr, Api, Binary, Env, StdError, StdResult, Uint64}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use minicbor_ser::{encode as cbor_encode, Encoder}; +use minicbor::{encode as cbor_encode, Encoder}; use crate::{encrypt_notification_data, get_seed, notification_id}; From 4912622ea352e3e34c9d5119ff5352d9c1bcbfdb Mon Sep 17 00:00:00 2001 From: Blake Regalia Date: Wed, 27 Nov 2024 08:37:21 -0800 Subject: [PATCH 23/29] dev: only pad if block size is given --- packages/notification/src/funcs.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/notification/src/funcs.rs b/packages/notification/src/funcs.rs index 621af37..52809fe 100644 --- a/packages/notification/src/funcs.rs +++ b/packages/notification/src/funcs.rs @@ -3,8 +3,6 @@ use secret_toolkit_crypto::{sha_256, hkdf_sha_256, HmacSha256}; use hkdf::hmac::Mac; use crate::cipher_data; -/// default notification block size in bytes -pub const NOTIFICATION_BLOCK_SIZE: usize = 36; pub const SEED_LEN: usize = 32; // 256 bits /// @@ -38,7 +36,9 @@ pub fn encrypt_notification_data( block_size: Option, ) -> StdResult { let mut padded_plaintext = plaintext.clone(); - zero_pad(&mut padded_plaintext, block_size.unwrap_or(NOTIFICATION_BLOCK_SIZE)); + if let Some(size) = block_size { + zero_pad(&mut padded_plaintext, size); + } let channel_id_bytes = sha_256(channel.as_bytes())[..12].to_vec(); let salt_bytes = hex::decode(tx_hash).unwrap()[..12].to_vec(); From ed701811343a466ab45e1d638731a23477adca3d Mon Sep 17 00:00:00 2001 From: Blake Regalia Date: Fri, 6 Dec 2024 15:12:19 -0800 Subject: [PATCH 24/29] dev: add cbor --- packages/notification/src/cbor.rs | 102 +++++++++++++++++++++++++++ packages/notification/src/funcs.rs | 24 +++++-- packages/notification/src/lib.rs | 4 +- packages/notification/src/structs.rs | 10 +-- 4 files changed, 126 insertions(+), 14 deletions(-) create mode 100644 packages/notification/src/cbor.rs diff --git a/packages/notification/src/cbor.rs b/packages/notification/src/cbor.rs new file mode 100644 index 0000000..b9a2213 --- /dev/null +++ b/packages/notification/src/cbor.rs @@ -0,0 +1,102 @@ +use cosmwasm_std::{CanonicalAddr, StdResult, StdError}; +use minicbor::{data as cbor_data, encode as cbor_encode, Encoder}; + +/// Length of encoding an arry header that holds less than 24 items +pub const CBL_ARRAY_SHORT: usize = 1; + +/// Length of encoding an arry header that holds between 24 and 255 items +pub const CBL_ARRAY_MEDIUM: usize = 2; + +/// Length of encoding an arry header that holds more than 255 items +pub const CBL_ARRAY_LARGE: usize = 3; + +/// Length of encoding a u8 value that is less than 24 +pub const CBL_U8_LESS_THAN_24: usize = 1; + +/// Length of encoding a u8 value that is greater than or equal to 24 +pub const CBL_U8: usize = 1 + 1; + +/// Length of encoding a u16 value +pub const CBL_U16: usize = 1 + 2; + +/// Length of encoding a u32 value +pub const CBL_U32: usize = 1 + 4; + +/// Length of encoding a u53 value (the maximum safe integer size for javascript) +pub const CBL_U53: usize = 1 + 8; + +/// Length of encoding a u64 value (with the bignum tag attached) +pub const CBL_BIGNUM_U64: usize = 1 + 1 + 8; + +// Length of encoding a timestamp +pub const CBL_TIMESTAMP: usize = 1 + 1 + 8; + +// Length of encoding a 20-byte canonical address +pub const CBL_ADDRESS: usize = 1 + 20; + +/// Wraps the CBOR error to CosmWasm StdError +pub fn cbor_to_std_error(e: cbor_encode::Error) -> StdError { + StdError::generic_err("CBOR encoding error") +} + +/// Extends the minicbor encoder with wrapper functions that handle CBOR errors +pub trait EncoderExt { + fn ext_tag(&mut self, tag: cbor_data::IanaTag) -> StdResult<&mut Self>; + + fn ext_u8(&mut self, value: u8) -> StdResult<&mut Self>; + fn ext_u32(&mut self, value: u32) -> StdResult<&mut Self>; + fn ext_u64_from_u128(&mut self, value: u128) -> StdResult<&mut Self>; + fn ext_address(&mut self, value: CanonicalAddr) -> StdResult<&mut Self>; + fn ext_bytes(&mut self, value: &[u8]) -> StdResult<&mut Self>; + fn ext_timestamp(&mut self, value: u64) -> StdResult<&mut Self>; +} + +impl EncoderExt for Encoder { + #[inline] + fn ext_tag(&mut self, tag: cbor_data::IanaTag) -> StdResult<&mut Self> { + self + .tag(cbor_data::Tag::from(tag)) + .map_err(cbor_to_std_error) + } + + #[inline] + fn ext_u8(&mut self, value: u8) -> StdResult<&mut Self> { + self + .u8(value) + .map_err(cbor_to_std_error) + } + + #[inline] + fn ext_u32(&mut self, value: u32) -> StdResult<&mut Self> { + self + .u32(value) + .map_err(cbor_to_std_error) + } + + #[inline] + fn ext_u64_from_u128(&mut self, value: u128) -> StdResult<&mut Self> { + self + .ext_tag(cbor_data::IanaTag::PosBignum)? + .ext_bytes(&value.to_be_bytes()[8..]) + } + + #[inline] + fn ext_address(&mut self, value: CanonicalAddr) -> StdResult<&mut Self> { + self.ext_bytes(&value.as_slice()) + } + + #[inline] + fn ext_bytes(&mut self, value: &[u8]) -> StdResult<&mut Self> { + self + .bytes(&value) + .map_err(cbor_to_std_error) + } + + #[inline] + fn ext_timestamp(&mut self, value: u64) -> StdResult<&mut Self> { + self + .ext_tag(cbor_data::IanaTag::Timestamp)? + .u64(value) + .map_err(cbor_to_std_error) + } +} \ No newline at end of file diff --git a/packages/notification/src/funcs.rs b/packages/notification/src/funcs.rs index 52809fe..f6d9431 100644 --- a/packages/notification/src/funcs.rs +++ b/packages/notification/src/funcs.rs @@ -14,11 +14,14 @@ pub fn notification_id(seed: &Binary, channel: &str, tx_hash: &String) -> StdRes // compute notification ID for this event let material = [channel.as_bytes(), ":".as_bytes(), tx_hash.to_ascii_uppercase().as_bytes()].concat(); + // create HMAC from seed let mut mac: HmacSha256 = HmacSha256::new_from_slice(seed.0.as_slice()).unwrap(); + + // add material to input stream mac.update(material.as_slice()); - let result = mac.finalize(); - let code_bytes = result.into_bytes(); - Ok(Binary::from(code_bytes.as_slice())) + + // finalize the digest and convert to CW Binary + Ok(Binary::from(mac.finalize().into_bytes().as_slice())) } /// @@ -35,18 +38,26 @@ pub fn encrypt_notification_data( plaintext: Vec, block_size: Option, ) -> StdResult { + // pad the plaintext to the optionally given block size let mut padded_plaintext = plaintext.clone(); if let Some(size) = block_size { - zero_pad(&mut padded_plaintext, size); + zero_pad_right(&mut padded_plaintext, size); } + // take the last 12 bytes of the channel name's hash to create the channel ID let channel_id_bytes = sha_256(channel.as_bytes())[..12].to_vec(); + + // take the last 12 bytes of the tx hash (after hex-decoding) to use for salt let salt_bytes = hex::decode(tx_hash).unwrap()[..12].to_vec(); + + // generate nonce by XOR'ing channel ID with salt let nonce: Vec = channel_id_bytes .iter() .zip(salt_bytes.iter()) .map(|(&b1, &b2)| b1 ^ b2) .collect(); + + // secure this message by attaching the block height and tx hash to the additional authenticated data let aad = format!("{}:{}", block_height, tx_hash); // encrypt notification data for this event @@ -68,11 +79,12 @@ pub fn get_seed(addr: &CanonicalAddr, secret: &[u8]) -> StdResult { addr.as_slice(), SEED_LEN, )?; + Ok(Binary::from(seed)) } -/// Take a Vec and pad it up to a multiple of `block_size`, using 0x00 at the end. -fn zero_pad(message: &mut Vec, block_size: usize) -> &mut Vec { +/// take a Vec and pad it up to a multiple of `block_size`, using 0x00 at the end +fn zero_pad_right(message: &mut Vec, block_size: usize) -> &mut Vec { let len = message.len(); let surplus = len % block_size; if surplus == 0 { diff --git a/packages/notification/src/lib.rs b/packages/notification/src/lib.rs index 3e25990..2161554 100644 --- a/packages/notification/src/lib.rs +++ b/packages/notification/src/lib.rs @@ -3,6 +3,8 @@ pub mod structs; pub mod funcs; pub mod cipher; +pub mod cbor; pub use structs::*; pub use funcs::*; -pub use cipher::*; \ No newline at end of file +pub use cipher::*; +pub use cbor::*; diff --git a/packages/notification/src/structs.rs b/packages/notification/src/structs.rs index 216948a..57a88db 100644 --- a/packages/notification/src/structs.rs +++ b/packages/notification/src/structs.rs @@ -3,7 +3,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use minicbor::{encode as cbor_encode, Encoder}; -use crate::{encrypt_notification_data, get_seed, notification_id}; +use crate::{encrypt_notification_data, get_seed, notification_id, cbor_to_std_error}; #[derive(Serialize, Debug, Deserialize, Clone)] #[cfg_attr(test, derive(Eq, PartialEq))] @@ -14,11 +14,6 @@ pub struct Notification { pub data: T, } - -pub fn cbor_to_std_error(e: cbor_encode::Error) -> StdError { - StdError::generic_err("CBOR encoding error") -} - pub trait NotificationData { const CHANNEL_ID: &'static str; const CDDL_SCHEMA: &'static str; @@ -46,7 +41,7 @@ pub trait NotificationData { // encode CBOR data self.encode_cbor(api, &mut encoder)?; - // return buffer + // return buffer (already right-padded with zero bytes) Ok(buffer) } @@ -55,6 +50,7 @@ pub trait NotificationData { } + impl Notification { pub fn new(notification_for: Addr, data: T) -> Self { Notification { From 0c65bf804d79da8f42fe61e4dd142a9cbb4ecefc Mon Sep 17 00:00:00 2001 From: Blake Regalia Date: Fri, 6 Dec 2024 15:15:36 -0800 Subject: [PATCH 25/29] dev: encoder-ext --- packages/notification/src/cbor.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/notification/src/cbor.rs b/packages/notification/src/cbor.rs index b9a2213..6360c38 100644 --- a/packages/notification/src/cbor.rs +++ b/packages/notification/src/cbor.rs @@ -51,7 +51,7 @@ pub trait EncoderExt { fn ext_timestamp(&mut self, value: u64) -> StdResult<&mut Self>; } -impl EncoderExt for Encoder { +pub impl EncoderExt for Encoder { #[inline] fn ext_tag(&mut self, tag: cbor_data::IanaTag) -> StdResult<&mut Self> { self @@ -99,4 +99,4 @@ impl EncoderExt for Encoder { .u64(value) .map_err(cbor_to_std_error) } -} \ No newline at end of file +} From 8602cb64d308368592f5afe9289d61a1a7b85717 Mon Sep 17 00:00:00 2001 From: Blake Regalia Date: Fri, 6 Dec 2024 15:21:12 -0800 Subject: [PATCH 26/29] fix: revert visibility modifier --- packages/notification/src/cbor.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/notification/src/cbor.rs b/packages/notification/src/cbor.rs index 6360c38..b9a2213 100644 --- a/packages/notification/src/cbor.rs +++ b/packages/notification/src/cbor.rs @@ -51,7 +51,7 @@ pub trait EncoderExt { fn ext_timestamp(&mut self, value: u64) -> StdResult<&mut Self>; } -pub impl EncoderExt for Encoder { +impl EncoderExt for Encoder { #[inline] fn ext_tag(&mut self, tag: cbor_data::IanaTag) -> StdResult<&mut Self> { self @@ -99,4 +99,4 @@ pub impl EncoderExt for Encoder { .u64(value) .map_err(cbor_to_std_error) } -} +} \ No newline at end of file From 0b5335e5c754bc09bc60eec59468bed76fed94d7 Mon Sep 17 00:00:00 2001 From: Blake Regalia Date: Sat, 7 Dec 2024 08:40:48 -0800 Subject: [PATCH 27/29] dev: group channel --- packages/notification/src/structs.rs | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/packages/notification/src/structs.rs b/packages/notification/src/structs.rs index 57a88db..14ab3dd 100644 --- a/packages/notification/src/structs.rs +++ b/packages/notification/src/structs.rs @@ -7,14 +7,14 @@ use crate::{encrypt_notification_data, get_seed, notification_id, cbor_to_std_er #[derive(Serialize, Debug, Deserialize, Clone)] #[cfg_attr(test, derive(Eq, PartialEq))] -pub struct Notification { +pub struct Notification { /// Recipient address of the notification pub notification_for: Addr, /// Typed notification data pub data: T, } -pub trait NotificationData { +pub trait DirectChannel { const CHANNEL_ID: &'static str; const CDDL_SCHEMA: &'static str; const ELEMENTS: u64; @@ -50,8 +50,7 @@ pub trait NotificationData { } - -impl Notification { +impl Notification { pub fn new(notification_for: Addr, data: T) -> Self { Notification { notification_for, @@ -176,4 +175,19 @@ pub struct FlatDescriptor { pub r#type: String, pub label: String, pub description: Option, -} \ No newline at end of file +} + +pub trait GroupChannel { + const CHANNEL_ID: &'static str; + const BLOOM_N: usize; + const BLOOM_M: u32; + const BLOOM_K: u32; + const PACKET_SIZE: usize; + + const BLOOM_M_LOG2: u32 = Self::BLOOM_M.ilog2(); + + fn build_packet(&self, api: &dyn Api, data: &D) -> StdResult>; + + fn notifications(&self) -> Vec>; +} + From 69eb5a00fca5f534d3210050fe4d3b3cbe681cb8 Mon Sep 17 00:00:00 2001 From: Blake Regalia Date: Sat, 7 Dec 2024 10:13:27 -0800 Subject: [PATCH 28/29] fix: return notifications as reference --- packages/notification/src/structs.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/notification/src/structs.rs b/packages/notification/src/structs.rs index 14ab3dd..212238a 100644 --- a/packages/notification/src/structs.rs +++ b/packages/notification/src/structs.rs @@ -188,6 +188,6 @@ pub trait GroupChannel { fn build_packet(&self, api: &dyn Api, data: &D) -> StdResult>; - fn notifications(&self) -> Vec>; + fn notifications(&self) -> &Vec>; } From b30831cdad594cdb7c147aac6f35e4a3165a1692 Mon Sep 17 00:00:00 2001 From: Blake Regalia Date: Sat, 7 Dec 2024 10:32:54 -0800 Subject: [PATCH 29/29] docs: release --- packages/notification/Cargo.toml | 4 ++-- packages/notification/Readme.md | 41 ++++++++++++++++---------------- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/packages/notification/Cargo.toml b/packages/notification/Cargo.toml index cf4bb87..99e8192 100644 --- a/packages/notification/Cargo.toml +++ b/packages/notification/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "secret-toolkit-notification" -version = "0.10.0" +version = "0.10.1" edition = "2021" authors = ["darwinzer0","blake-regalia"] license-file = "../../LICENSE" @@ -30,6 +30,6 @@ primitive-types = { version = "0.12.2", default-features = false } hex = "0.4.3" minicbor = "0.25.1" -secret-toolkit-crypto = { version = "0.10.0", path = "../crypto", features = [ +secret-toolkit-crypto = { version = "0.10.1", path = "../crypto", features = [ "hash", "hkdf" ] } \ No newline at end of file diff --git a/packages/notification/Readme.md b/packages/notification/Readme.md index 4c305c0..f3375d3 100644 --- a/packages/notification/Readme.md +++ b/packages/notification/Readme.md @@ -4,42 +4,43 @@ These functions are meant to help you easily create notification channels for private push notifications in secret contracts (see [SNIP-52 Private Push Notification](https://github.com/SolarRepublic/SNIPs/blob/feat/snip-52/SNIP-52.md)). -### Implementing a `NotificationData` struct +### Implementing a `DirectChannel` struct -Each notification channel will have a specified data format, which is defined by creating a struct that implements the `NotificationData` trait, which has two methods: `to_cbor` and `channel_id`. +Each notification channel will have a specified data format, which is defined by creating a struct that implements the `DirectChannel` trait, which has one method: `encode_cbor`. -The following example illustrates how you might implement this for a channel called `my_channel` and notification data containing two fields: `message` and `amount`. +The following example illustrates how you might implement this for a channel called `my_channel` and notification data containing two fields: `sender` and `amount`. ```rust use cosmwasm_std::{Api, StdError, StdResult}; -use secret_toolkit::notification::NotificationData; +use secret_toolkit::notification::{EncoderExt, CBL_ARRAY_SHORT, CBL_BIGNUM_U64, CBL_U8, Notification, DirectChannel, GroupChannel}; use serde::{Deserialize, Serialize}; use minicbor_ser as cbor; #[derive(Serialize, Debug, Deserialize, Clone)] -pub struct MyNotificationData { - pub message: String, +pub struct MyNotification { + pub sender: Addr, pub amount: u128, } -impl NotificationData for MyNotificationData { - fn to_cbor(&self, _api: &dyn Api) -> StdResult> { - let my_data = cbor::to_vec(&( - self.message.as_bytes(), - self.amount.to_be_bytes(), - )) - .map_err(|e| StdError::generic_err(format!("{:?}", e)))?; +impl DirectChannel for MyNotification { + const CHANNEL_ID: &'static str = "my_channel"; + const CDDL_SCHEMA: &'static str = "my_channel=[sender:bstr .size 20,amount:uint .size 8]"; + const ELEMENTS: u64 = 2; + const PAYLOAD_SIZE: usize = CBL_ARRAY_SHORT + CBL_BIGNUM_U64 + CBL_U8; - Ok(my_data) - } + fn encode_cbor(&self, api: &dyn Api, encoder: &mut Encoder<&mut [u8]>) -> StdResult<()> { + // amount:biguint (8-byte uint) + encoder.ext_u64_from_u128(self.amount)?; + + // sender:bstr (20-byte address) + let sender_raw = api.addr_canonicalize(sender.as_str())?; + encoder.ext_address(sender_raw)?; - fn channel_id(&self) -> &str { - "my_channel" + Ok(()) } } ``` -The `api` parameter for `to_cbor` is not used in this example, but is there for cases where you might want to convert an `Addr` to a `CanonicalAddr` before encoding using CBOR. ### Sending a TxHash notification @@ -50,8 +51,8 @@ The following code snippet creates a notification for the above `my_channel` and ```rust let notification = Notification::new( recipient, - MyNotificationData { - "hello".to_string(), + MyNotification { + sender, 1000_u128, } )