Skip to main content

walletconnect_sdk/
message.rs

1/// Message
2///
3/// Logic to encrypt and decrypt raw payload, which can be sent over the IRN.
4///
5use aes_gcm::aead::{Aead, KeyInit, OsRng};
6use aes_gcm::{Key, Nonce};
7use alloy::hex;
8use alloy::primitives::Bytes;
9use base64ct::{Base64, Base64UrlUnpadded, Encoding};
10use chacha20poly1305::ChaCha20Poly1305;
11use log::debug;
12use rand::RngCore;
13use serde::de::DeserializeOwned;
14use serde::{Deserialize, Serialize};
15use serde_json::Value;
16
17use crate::error::Result;
18use crate::types::Id;
19
20#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
21pub struct MessageError {
22    pub message: Option<String>,
23    pub code: Option<i64>,
24    pub data: Option<Bytes>,
25}
26
27#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
28pub struct Message<M = String, T = Value> {
29    pub jsonrpc: String,
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub method: Option<M>,
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub params: Option<T>,
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub result: Option<T>,
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub error: Option<MessageError>,
38    pub id: Id,
39}
40
41pub const IV_LENGTH: usize = 12;
42pub const KEY_LENGTH: usize = 32;
43pub const TYPE_LENGTH: usize = 1;
44pub const TYPE_0: u8 = 0;
45pub const TYPE_1: u8 = 1;
46pub const TYPE_2: u8 = 2;
47
48impl<T: Serialize + DeserializeOwned> Message<String, T> {
49    pub fn result(result: T, id: Id) -> Message<String, T> {
50        Message {
51            jsonrpc: "2.0".to_string(),
52            method: None,
53            params: None,
54            result: Some(result),
55            error: None,
56            id,
57        }
58    }
59}
60
61impl<
62    M: PartialEq + Serialize + DeserializeOwned,
63    T: Serialize + DeserializeOwned,
64> Message<M, T>
65{
66    // https://github.com/WalletConnect/walletconnect-monorepo/blob/7bcb116d17a76a9b61cd5b90ebd2087411f48f53/packages/utils/src/crypto.ts#L86
67    pub fn encrypt(
68        &self,
69        sym_key: [u8; 32],
70        type_byte: Option<u8>,
71        sender_public_key: Option<String>,
72        encoding: Option<EncodingType>,
73    ) -> Result<String> {
74        let type_byte = type_byte.unwrap_or(TYPE_0);
75        if type_byte == TYPE_1 && sender_public_key.is_none() {
76            return Err("Missing sender public key for type 1 envelope".into());
77        }
78
79        let mut iv = vec![0u8; IV_LENGTH];
80        OsRng.fill_bytes(&mut iv);
81
82        let key = Key::<ChaCha20Poly1305>::from_slice(&sym_key);
83        let cipher = ChaCha20Poly1305::new(key);
84        let nonce = Nonce::from_slice(&iv);
85
86        let message = serde_json::to_string(self)?;
87        debug!("encrypting message -> {message}");
88        let sealed = cipher
89            .encrypt(nonce, message.as_bytes())
90            .expect("encryption failed");
91
92        Ok(EncryptedEnvelope {
93            type_byte,
94            sealed,
95            iv,
96            sender_public_key: sender_public_key.as_ref().map(|hex| {
97                hex::decode(hex).expect("invalid sender_public_key")
98            }),
99        }
100        .serialize(encoding.clone().unwrap_or(EncodingType::Base64)))
101    }
102}
103
104impl<M, P> Message<M, P>
105where
106    M: PartialEq,
107{
108    pub fn is(&self, method: M) -> bool {
109        self.method == Some(method)
110    }
111
112    pub fn create_success_response<R>(
113        &self,
114        result_data: R,
115    ) -> Message<String, R> {
116        Message {
117            jsonrpc: self.jsonrpc.clone(),
118            method: None,
119            params: None,
120            result: Some(result_data),
121            error: None,
122            id: self.id.clone(),
123        }
124    }
125
126    pub fn create_error_response(
127        &self,
128        message: String,
129        code: i64,
130        data: Option<Bytes>,
131    ) -> Message<M, Value> {
132        Message {
133            jsonrpc: self.jsonrpc.clone(),
134            method: None,
135            params: None,
136            result: None,
137            error: Some(MessageError {
138                message: Some(message),
139                code: Some(code),
140                data,
141            }),
142            id: self.id.clone(),
143        }
144    }
145}
146
147impl Message<String, Value> {
148    // https://github.com/WalletConnect/walletconnect-monorepo/blob/7bcb116d17a76a9b61cd5b90ebd2087411f48f53/packages/utils/src/crypto.ts#L105
149    pub fn decrypt(
150        cipher_text: &str,
151        sym_key: [u8; 32],
152        encoding: Option<EncodingType>,
153    ) -> Result<Self> {
154        let key = Key::<ChaCha20Poly1305>::from_slice(&sym_key);
155
156        let encoding_params = EncryptedEnvelope::deserialize(
157            cipher_text,
158            encoding.clone().unwrap_or(EncodingType::Base64),
159        );
160
161        let cipher = ChaCha20Poly1305::new(key);
162        let nonce = Nonce::from_slice(&encoding_params.iv);
163        let decrypted =
164            cipher.decrypt(nonce, encoding_params.sealed.as_ref())?;
165        let str = String::from_utf8(decrypted)?;
166        debug!("decrypted message -> {str}");
167        Ok(serde_json::from_str::<Self>(&str).map_err(|e| {
168            format!("Failed to deserialize JSON-RPC request: {e}\n{str}")
169        })?)
170    }
171
172    pub fn try_decode<M, P>(&self) -> Result<Message<M, P>>
173    where
174        M: DeserializeOwned + Clone,
175        P: DeserializeOwned,
176    {
177        let method = self
178            .method
179            .as_ref()
180            .map(|m| serde_plain::from_str::<M>(m.as_str()))
181            .transpose()?;
182        let params = self
183            .params
184            .as_ref()
185            .map(|p| serde_json::from_value::<P>(p.clone()))
186            .transpose()?;
187        let result = self
188            .result
189            .as_ref()
190            .map(|r| serde_json::from_value::<P>(r.clone()))
191            .transpose()?;
192
193        Ok(Message {
194            jsonrpc: self.jsonrpc.clone(),
195            method,
196            params,
197            result,
198            error: self.error.clone(),
199            id: self.id.clone(),
200        })
201    }
202
203    pub fn decode_result<R>(&self) -> Result<Message<String, R>>
204    where
205        R: DeserializeOwned,
206    {
207        let result = self
208            .result
209            .as_ref()
210            .map(|r| serde_json::from_value::<R>(r.clone()))
211            .transpose()?;
212        Ok(Message {
213            jsonrpc: self.jsonrpc.clone(),
214            method: None,
215            params: None,
216            result,
217            error: None,
218            id: self.id.clone(),
219        })
220    }
221
222    pub fn into_value(self) -> Result<Value> {
223        Ok(serde_json::to_value(self)?)
224    }
225}
226
227#[derive(Debug)]
228pub struct EncryptedEnvelope {
229    pub type_byte: u8,
230    pub sealed: Vec<u8>,
231    pub iv: Vec<u8>,
232    // only for type 1 message - helps dapp to calculate diffie_sym_key
233    pub sender_public_key: Option<Vec<u8>>,
234}
235
236#[derive(Debug, Clone)]
237pub enum EncodingType {
238    Base64,
239    Base64Url,
240}
241
242impl EncryptedEnvelope {
243    // https://github.com/WalletConnect/walletconnect-monorepo/blob/b39a5d4e62f5517ef47a70b5b93f27585b7132e8/packages/core/src/controllers/crypto.ts#L111
244    pub fn serialize(&self, encoding: EncodingType) -> String {
245        let mut bytes = vec![self.type_byte];
246
247        match self.type_byte {
248            TYPE_2 => {
249                bytes.extend_from_slice(&self.sealed);
250            }
251            TYPE_1 => {
252                let sender = self
253                    .sender_public_key
254                    .as_ref()
255                    .expect("Missing sender public key for type 1 envelope");
256                bytes.extend_from_slice(sender);
257                bytes.extend_from_slice(&self.iv);
258                bytes.extend_from_slice(&self.sealed);
259            }
260            _ => {
261                // TYPE_0
262                bytes.extend_from_slice(&self.iv);
263                bytes.extend_from_slice(&self.sealed);
264            }
265        }
266
267        match encoding {
268            EncodingType::Base64 => Base64::encode_string(&bytes),
269            EncodingType::Base64Url => Base64UrlUnpadded::encode_string(&bytes),
270        }
271    }
272
273    // https://github.com/WalletConnect/walletconnect-monorepo/blob/b39a5d4e62f5517ef47a70b5b93f27585b7132e8/packages/core/src/controllers/crypto.ts#L131
274    pub fn deserialize(encoded: &str, encoding: EncodingType) -> Self {
275        let bytes = match encoding {
276            EncodingType::Base64 => {
277                Base64::decode_vec(encoded).expect("invalid base64")
278            }
279            EncodingType::Base64Url => Base64UrlUnpadded::decode_vec(encoded)
280                .expect("invalid base64url"),
281        };
282
283        let type_byte = bytes[0];
284        let slice1 = TYPE_LENGTH;
285
286        match type_byte {
287            TYPE_1 => {
288                let slice2 = slice1 + KEY_LENGTH;
289                let slice3 = slice2 + IV_LENGTH;
290                let sender_public_key = bytes[slice1..slice2].to_vec();
291                let iv = bytes[slice2..slice3].to_vec();
292                let sealed = bytes[slice3..].to_vec();
293                EncryptedEnvelope {
294                    type_byte,
295                    sealed,
296                    iv,
297                    sender_public_key: Some(sender_public_key),
298                }
299            }
300            TYPE_2 => {
301                let sealed = bytes[slice1..].to_vec();
302                let mut iv = vec![0u8; IV_LENGTH];
303                OsRng.fill_bytes(&mut iv);
304                EncryptedEnvelope {
305                    type_byte,
306                    sealed,
307                    iv,
308                    sender_public_key: None,
309                }
310            }
311            _ => {
312                // TYPE_0 default
313                let slice2 = slice1 + IV_LENGTH;
314                let iv = bytes[slice1..slice2].to_vec();
315                let sealed = bytes[slice2..].to_vec();
316                EncryptedEnvelope {
317                    type_byte,
318                    sealed,
319                    iv,
320                    sender_public_key: None,
321                }
322            }
323        }
324    }
325}
326
327#[cfg(test)]
328mod tests {
329    use serde_json::{Number, json};
330
331    use super::*;
332    use crate::message::TYPE_1;
333    use crate::{relay_auth::Keypair, types::Id, utils::derive_sym_key};
334
335    #[test]
336    fn test_encrypt_decrypt() {
337        let key = Keypair::generate();
338        let sym_key = derive_sym_key(key.seed, key.public_key);
339
340        let message = Message::result(
341            json!({
342                "cacao": {
343                    "header": {
344                        "h": "caip122"
345                    },
346                    "payload": {
347                        "iss": {
348                            "account_address": hex::encode(key.public_key),
349                            "chain_id": "eip155"
350                        },
351                        "domain": "https://example.com",
352                        "uri": "wc:1234@2?relay-protocol=irn",
353                        "version": "1.0.0",
354                        "statement": "Please sign this message to authenticate",
355                        "nonce": hex::encode(key.seed),
356                    },
357                    "signature": {
358                        "t": "eip191",
359                    }
360                }
361            }),
362            Id::Number(Number::from(12345)),
363        );
364
365        let encrypted_message = message
366            .encrypt(
367                sym_key,
368                Some(TYPE_1),
369                Some(hex::encode(key.public_key)),
370                None,
371            )
372            .unwrap();
373
374        let decrypted_message =
375            Message::decrypt(&encrypted_message, sym_key, None).unwrap();
376
377        assert_eq!(message.jsonrpc, decrypted_message.jsonrpc);
378    }
379}