Skip to content

Commit

Permalink
Merge pull request #99 from SolarRepublic/master
Browse files Browse the repository at this point in the history
SNIP-52 notification package + HKDF crypto funcs
  • Loading branch information
iKapitonau authored Dec 11, 2024
2 parents 9b15d02 + b30831c commit ef91e07
Show file tree
Hide file tree
Showing 12 changed files with 582 additions and 1 deletion.
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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.1", path = "packages/crypto", optional = true }
Expand All @@ -45,7 +46,7 @@ secret-toolkit-snip721 = { version = "0.10.1", path = "packages/snip721", option
secret-toolkit-storage = { version = "0.10.1", path = "packages/storage", optional = true }
secret-toolkit-utils = { version = "0.10.1", path = "packages/utils", optional = true }
secret-toolkit-viewing-key = { version = "0.10.1", path = "packages/viewing_key", optional = true }

secret-toolkit-notification = { version = "0.10.1", path = "packages/notification", optional = true }

[workspace]
members = ["packages/*"]
Expand Down
2 changes: 2 additions & 0 deletions packages/crypto/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand All @@ -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 }
cc = { version = "=1.1.10" }

Expand Down
40 changes: 40 additions & 0 deletions packages/crypto/src/hkdf.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
use cosmwasm_std::{StdError, StdResult};
use hkdf::{hmac::Hmac, Hkdf};
use sha2::{Sha256, Sha512};

// Create alias for HMAC-SHA256
pub type HmacSha256 = Hmac<Sha256>;

pub fn hkdf_sha_256(
salt: &Option<Vec<u8>>,
ikm: &[u8],
info: &[u8],
length: usize,
) -> StdResult<Vec<u8>> {
let hk: Hkdf<Sha256> = Hkdf::<Sha256>::new(salt.as_deref(), ikm);
let mut zero_bytes = vec![0u8; length];
let okm = zero_bytes.as_mut_slice();
match hk.expand(info, okm) {
Ok(_) => Ok(okm.to_vec()),
Err(e) => {
Err(StdError::generic_err(format!("{:?}", e)))
}
}
}

pub fn hkdf_sha_512(
salt: &Option<Vec<u8>>,
ikm: &[u8],
info: &[u8],
length: usize,
) -> StdResult<Vec<u8>> {
let hk: Hkdf<Sha512> = Hkdf::<Sha512>::new(salt.as_deref(), ikm);
let mut zero_bytes = vec![0u8; length];
let okm = zero_bytes.as_mut_slice();
match hk.expand(info, okm) {
Ok(_) => Ok(okm.to_vec()),
Err(e) => {
Err(StdError::generic_err(format!("{:?}", e)))
}
}
}
5 changes: 5 additions & 0 deletions packages/crypto/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
35 changes: 35 additions & 0 deletions packages/notification/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
[package]
name = "secret-toolkit-notification"
version = "0.10.1"
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 }

# rand_core = { version = "0.6.4", default-features = false }
# rand_chacha = { version = "0.3.1", default-features = false }
sha2 = "0.10.6"
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 }
hex = "0.4.3"
minicbor = "0.25.1"

secret-toolkit-crypto = { version = "0.10.1", path = "../crypto", features = [
"hash", "hkdf"
] }
72 changes: 72 additions & 0 deletions packages/notification/Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# 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 (see [SNIP-52 Private Push Notification](https://github.com/SolarRepublic/SNIPs/blob/feat/snip-52/SNIP-52.md)).

### Implementing a `DirectChannel` struct

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: `sender` and `amount`.

```rust
use cosmwasm_std::{Api, StdError, StdResult};
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 MyNotification {
pub sender: Addr,
pub amount: u128,
}

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;

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)?;

Ok(())
}
}
```


### Sending a TxHash notification

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 notification = Notification::new(
recipient,
MyNotification {
sender,
1000_u128,
}
)
.to_txhash_notification(deps.api, &env, secret)?;

// ... other code

// add notification to response
Ok(Response::new()
.set_data(to_binary(&ExecuteAnswer::MyMessage { status: Success } )?)
.add_attribute_plaintext(
notification.id_plaintext(),
notification.data_plaintext(),
)
)
```

102 changes: 102 additions & 0 deletions packages/notification/src/cbor.rs
Original file line number Diff line number Diff line change
@@ -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<T>(e: cbor_encode::Error<T>) -> 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<T: cbor_encode::Write> EncoderExt for Encoder<T> {
#[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)
}
}
20 changes: 20 additions & 0 deletions packages/notification/src/cipher.rs
Original file line number Diff line number Diff line change
@@ -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<Vec<u8>> {
let cipher = ChaCha20Poly1305::new_from_slice(key)
.map_err(|e| StdError::generic_err(format!("{:?}", e)))?;
let mut buffer: Vec<u8> = 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<u8> {
vec1.iter().zip(vec2.iter()).map(|(&a, &b)| a ^ b).collect()
}
Loading

0 comments on commit ef91e07

Please sign in to comment.