Skip to content

Commit ef91e07

Browse files
authored
Merge pull request #99 from SolarRepublic/master
SNIP-52 notification package + HKDF crypto funcs
2 parents 9b15d02 + b30831c commit ef91e07

File tree

12 files changed

+582
-1
lines changed

12 files changed

+582
-1
lines changed

Cargo.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ snip721 = ["secret-toolkit-snip721", "utils"]
3434
storage = ["secret-toolkit-storage", "serialization"]
3535
utils = ["secret-toolkit-utils"]
3636
viewing-key = ["secret-toolkit-viewing-key"]
37+
notification = ["secret-toolkit-notification"]
3738

3839
[dependencies]
3940
secret-toolkit-crypto = { version = "0.10.1", path = "packages/crypto", optional = true }
@@ -45,7 +46,7 @@ secret-toolkit-snip721 = { version = "0.10.1", path = "packages/snip721", option
4546
secret-toolkit-storage = { version = "0.10.1", path = "packages/storage", optional = true }
4647
secret-toolkit-utils = { version = "0.10.1", path = "packages/utils", optional = true }
4748
secret-toolkit-viewing-key = { version = "0.10.1", path = "packages/viewing_key", optional = true }
48-
49+
secret-toolkit-notification = { version = "0.10.1", path = "packages/notification", optional = true }
4950

5051
[workspace]
5152
members = ["packages/*"]

packages/crypto/Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ default = ["hash", "ecc-secp256k1", "rand"]
1818
hash = ["sha2"]
1919
ecc-secp256k1 = ["secp256k1"]
2020
rand = ["hash", "rand_chacha", "rand_core"]
21+
hkdf = ["sha2"]
2122

2223
[dependencies]
2324
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 }
2627
secp256k1 = { version = "0.27.0", default-features = false, features = [
2728
"alloc",
2829
], optional = true }
30+
hkdf = "0.12.3"
2931
cosmwasm-std = { workspace = true }
3032
cc = { version = "=1.1.10" }
3133

packages/crypto/src/hkdf.rs

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
use cosmwasm_std::{StdError, StdResult};
2+
use hkdf::{hmac::Hmac, Hkdf};
3+
use sha2::{Sha256, Sha512};
4+
5+
// Create alias for HMAC-SHA256
6+
pub type HmacSha256 = Hmac<Sha256>;
7+
8+
pub fn hkdf_sha_256(
9+
salt: &Option<Vec<u8>>,
10+
ikm: &[u8],
11+
info: &[u8],
12+
length: usize,
13+
) -> StdResult<Vec<u8>> {
14+
let hk: Hkdf<Sha256> = Hkdf::<Sha256>::new(salt.as_deref(), ikm);
15+
let mut zero_bytes = vec![0u8; length];
16+
let okm = zero_bytes.as_mut_slice();
17+
match hk.expand(info, okm) {
18+
Ok(_) => Ok(okm.to_vec()),
19+
Err(e) => {
20+
Err(StdError::generic_err(format!("{:?}", e)))
21+
}
22+
}
23+
}
24+
25+
pub fn hkdf_sha_512(
26+
salt: &Option<Vec<u8>>,
27+
ikm: &[u8],
28+
info: &[u8],
29+
length: usize,
30+
) -> StdResult<Vec<u8>> {
31+
let hk: Hkdf<Sha512> = Hkdf::<Sha512>::new(salt.as_deref(), ikm);
32+
let mut zero_bytes = vec![0u8; length];
33+
let okm = zero_bytes.as_mut_slice();
34+
match hk.expand(info, okm) {
35+
Ok(_) => Ok(okm.to_vec()),
36+
Err(e) => {
37+
Err(StdError::generic_err(format!("{:?}", e)))
38+
}
39+
}
40+
}

packages/crypto/src/lib.rs

+5
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,8 @@ pub use hash::{sha_256, SHA256_HASH_SIZE};
1212

1313
#[cfg(feature = "rand")]
1414
pub use rng::ContractPrng;
15+
16+
#[cfg(feature = "hkdf")]
17+
pub mod hkdf;
18+
#[cfg(feature = "hkdf")]
19+
pub use crate::hkdf::*;

packages/notification/Cargo.toml

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
[package]
2+
name = "secret-toolkit-notification"
3+
version = "0.10.1"
4+
edition = "2021"
5+
authors = ["darwinzer0","blake-regalia"]
6+
license-file = "../../LICENSE"
7+
repository = "https://github.com/scrtlabs/secret-toolkit"
8+
readme = "Readme.md"
9+
description = "Helper tools for SNIP-52 notifications in Secret Contracts"
10+
categories = ["cryptography::cryptocurrencies", "wasm"]
11+
keywords = ["secret-network", "secret-contracts", "secret-toolkit"]
12+
13+
[package.metadata.docs.rs]
14+
all-features = true
15+
16+
[dependencies]
17+
cosmwasm-std = { workspace = true, version = "1.0.0" }
18+
serde = { workspace = true }
19+
20+
ripemd = { version = "0.1.3", default-features = false }
21+
schemars = { workspace = true }
22+
23+
# rand_core = { version = "0.6.4", default-features = false }
24+
# rand_chacha = { version = "0.3.1", default-features = false }
25+
sha2 = "0.10.6"
26+
chacha20poly1305 = { version = "0.10.1", default-features = false, features = ["alloc", "rand_core"] }
27+
generic-array = "0.14.7"
28+
hkdf = "0.12.3"
29+
primitive-types = { version = "0.12.2", default-features = false }
30+
hex = "0.4.3"
31+
minicbor = "0.25.1"
32+
33+
secret-toolkit-crypto = { version = "0.10.1", path = "../crypto", features = [
34+
"hash", "hkdf"
35+
] }

packages/notification/Readme.md

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Secret Contract Development Toolkit - SNIP52 (Private Push Notification) Interface
2+
3+
⚠️ This package is a sub-package of the `secret-toolkit` package. Please see its crate page for more context.
4+
5+
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)).
6+
7+
### Implementing a `DirectChannel` struct
8+
9+
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`.
10+
11+
The following example illustrates how you might implement this for a channel called `my_channel` and notification data containing two fields: `sender` and `amount`.
12+
13+
```rust
14+
use cosmwasm_std::{Api, StdError, StdResult};
15+
use secret_toolkit::notification::{EncoderExt, CBL_ARRAY_SHORT, CBL_BIGNUM_U64, CBL_U8, Notification, DirectChannel, GroupChannel};
16+
use serde::{Deserialize, Serialize};
17+
use minicbor_ser as cbor;
18+
19+
#[derive(Serialize, Debug, Deserialize, Clone)]
20+
pub struct MyNotification {
21+
pub sender: Addr,
22+
pub amount: u128,
23+
}
24+
25+
impl DirectChannel for MyNotification {
26+
const CHANNEL_ID: &'static str = "my_channel";
27+
const CDDL_SCHEMA: &'static str = "my_channel=[sender:bstr .size 20,amount:uint .size 8]";
28+
const ELEMENTS: u64 = 2;
29+
const PAYLOAD_SIZE: usize = CBL_ARRAY_SHORT + CBL_BIGNUM_U64 + CBL_U8;
30+
31+
fn encode_cbor(&self, api: &dyn Api, encoder: &mut Encoder<&mut [u8]>) -> StdResult<()> {
32+
// amount:biguint (8-byte uint)
33+
encoder.ext_u64_from_u128(self.amount)?;
34+
35+
// sender:bstr (20-byte address)
36+
let sender_raw = api.addr_canonicalize(sender.as_str())?;
37+
encoder.ext_address(sender_raw)?;
38+
39+
Ok(())
40+
}
41+
}
42+
```
43+
44+
45+
### Sending a TxHash notification
46+
47+
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.
48+
49+
The following code snippet creates a notification for the above `my_channel` and adds it to the contract `Response` as a plaintext attribute.
50+
51+
```rust
52+
let notification = Notification::new(
53+
recipient,
54+
MyNotification {
55+
sender,
56+
1000_u128,
57+
}
58+
)
59+
.to_txhash_notification(deps.api, &env, secret)?;
60+
61+
// ... other code
62+
63+
// add notification to response
64+
Ok(Response::new()
65+
.set_data(to_binary(&ExecuteAnswer::MyMessage { status: Success } )?)
66+
.add_attribute_plaintext(
67+
notification.id_plaintext(),
68+
notification.data_plaintext(),
69+
)
70+
)
71+
```
72+

packages/notification/src/cbor.rs

+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
use cosmwasm_std::{CanonicalAddr, StdResult, StdError};
2+
use minicbor::{data as cbor_data, encode as cbor_encode, Encoder};
3+
4+
/// Length of encoding an arry header that holds less than 24 items
5+
pub const CBL_ARRAY_SHORT: usize = 1;
6+
7+
/// Length of encoding an arry header that holds between 24 and 255 items
8+
pub const CBL_ARRAY_MEDIUM: usize = 2;
9+
10+
/// Length of encoding an arry header that holds more than 255 items
11+
pub const CBL_ARRAY_LARGE: usize = 3;
12+
13+
/// Length of encoding a u8 value that is less than 24
14+
pub const CBL_U8_LESS_THAN_24: usize = 1;
15+
16+
/// Length of encoding a u8 value that is greater than or equal to 24
17+
pub const CBL_U8: usize = 1 + 1;
18+
19+
/// Length of encoding a u16 value
20+
pub const CBL_U16: usize = 1 + 2;
21+
22+
/// Length of encoding a u32 value
23+
pub const CBL_U32: usize = 1 + 4;
24+
25+
/// Length of encoding a u53 value (the maximum safe integer size for javascript)
26+
pub const CBL_U53: usize = 1 + 8;
27+
28+
/// Length of encoding a u64 value (with the bignum tag attached)
29+
pub const CBL_BIGNUM_U64: usize = 1 + 1 + 8;
30+
31+
// Length of encoding a timestamp
32+
pub const CBL_TIMESTAMP: usize = 1 + 1 + 8;
33+
34+
// Length of encoding a 20-byte canonical address
35+
pub const CBL_ADDRESS: usize = 1 + 20;
36+
37+
/// Wraps the CBOR error to CosmWasm StdError
38+
pub fn cbor_to_std_error<T>(e: cbor_encode::Error<T>) -> StdError {
39+
StdError::generic_err("CBOR encoding error")
40+
}
41+
42+
/// Extends the minicbor encoder with wrapper functions that handle CBOR errors
43+
pub trait EncoderExt {
44+
fn ext_tag(&mut self, tag: cbor_data::IanaTag) -> StdResult<&mut Self>;
45+
46+
fn ext_u8(&mut self, value: u8) -> StdResult<&mut Self>;
47+
fn ext_u32(&mut self, value: u32) -> StdResult<&mut Self>;
48+
fn ext_u64_from_u128(&mut self, value: u128) -> StdResult<&mut Self>;
49+
fn ext_address(&mut self, value: CanonicalAddr) -> StdResult<&mut Self>;
50+
fn ext_bytes(&mut self, value: &[u8]) -> StdResult<&mut Self>;
51+
fn ext_timestamp(&mut self, value: u64) -> StdResult<&mut Self>;
52+
}
53+
54+
impl<T: cbor_encode::Write> EncoderExt for Encoder<T> {
55+
#[inline]
56+
fn ext_tag(&mut self, tag: cbor_data::IanaTag) -> StdResult<&mut Self> {
57+
self
58+
.tag(cbor_data::Tag::from(tag))
59+
.map_err(cbor_to_std_error)
60+
}
61+
62+
#[inline]
63+
fn ext_u8(&mut self, value: u8) -> StdResult<&mut Self> {
64+
self
65+
.u8(value)
66+
.map_err(cbor_to_std_error)
67+
}
68+
69+
#[inline]
70+
fn ext_u32(&mut self, value: u32) -> StdResult<&mut Self> {
71+
self
72+
.u32(value)
73+
.map_err(cbor_to_std_error)
74+
}
75+
76+
#[inline]
77+
fn ext_u64_from_u128(&mut self, value: u128) -> StdResult<&mut Self> {
78+
self
79+
.ext_tag(cbor_data::IanaTag::PosBignum)?
80+
.ext_bytes(&value.to_be_bytes()[8..])
81+
}
82+
83+
#[inline]
84+
fn ext_address(&mut self, value: CanonicalAddr) -> StdResult<&mut Self> {
85+
self.ext_bytes(&value.as_slice())
86+
}
87+
88+
#[inline]
89+
fn ext_bytes(&mut self, value: &[u8]) -> StdResult<&mut Self> {
90+
self
91+
.bytes(&value)
92+
.map_err(cbor_to_std_error)
93+
}
94+
95+
#[inline]
96+
fn ext_timestamp(&mut self, value: u64) -> StdResult<&mut Self> {
97+
self
98+
.ext_tag(cbor_data::IanaTag::Timestamp)?
99+
.u64(value)
100+
.map_err(cbor_to_std_error)
101+
}
102+
}

packages/notification/src/cipher.rs

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
use chacha20poly1305::{
2+
aead::{AeadInPlace, KeyInit},
3+
ChaCha20Poly1305,
4+
};
5+
use cosmwasm_std::{StdError, StdResult};
6+
use generic_array::GenericArray;
7+
8+
pub fn cipher_data(key: &[u8], nonce: &[u8], plaintext: &[u8], aad: &[u8]) -> StdResult<Vec<u8>> {
9+
let cipher = ChaCha20Poly1305::new_from_slice(key)
10+
.map_err(|e| StdError::generic_err(format!("{:?}", e)))?;
11+
let mut buffer: Vec<u8> = plaintext.to_vec();
12+
cipher
13+
.encrypt_in_place(GenericArray::from_slice(nonce), aad, &mut buffer)
14+
.map_err(|e| StdError::generic_err(format!("{:?}", e)))?;
15+
Ok(buffer)
16+
}
17+
18+
pub fn xor_bytes(vec1: &[u8], vec2: &[u8]) -> Vec<u8> {
19+
vec1.iter().zip(vec2.iter()).map(|(&a, &b)| a ^ b).collect()
20+
}

0 commit comments

Comments
 (0)