1use base64::{engine::general_purpose::STANDARD, Engine};
2use bsv::primitives::public_key::PublicKey;
3use bsv::wallet::interfaces::{
4 CreateHmacArgs, DecryptArgs, EncryptArgs, WalletInterface,
5};
6use bsv::wallet::types::{Counterparty, CounterpartyType, Protocol};
7
8use crate::error::MessageBoxError;
9
10pub async fn encrypt_body<W: WalletInterface>(
16 wallet: &W,
17 body: &str,
18 recipient_pubkey_hex: &str,
19 originator: Option<&str>,
20) -> Result<String, MessageBoxError> {
21 let pk = PublicKey::from_string(recipient_pubkey_hex)
22 .map_err(|e| MessageBoxError::Encryption(e.to_string()))?;
23
24 let result = wallet
25 .encrypt(
26 EncryptArgs {
27 protocol_id: Protocol {
28 security_level: 1,
29 protocol: "messagebox".to_string(),
30 },
31 key_id: "1".to_string(),
32 counterparty: Counterparty {
33 counterparty_type: CounterpartyType::Other,
34 public_key: Some(pk),
35 },
36 plaintext: body.as_bytes().to_vec(),
37 privileged: false,
38 privileged_reason: None,
39 seek_permission: None,
40 },
41 originator,
42 )
43 .await
44 .map_err(|e| MessageBoxError::Wallet(e.to_string()))?;
45
46 let b64 = STANDARD.encode(&result.ciphertext);
48 Ok(serde_json::json!({"encryptedMessage": b64}).to_string())
49}
50
51pub async fn decrypt_body<W: WalletInterface>(
56 wallet: &W,
57 encrypted_json: &str,
58 sender_pubkey_hex: &str,
59 originator: Option<&str>,
60) -> Result<String, MessageBoxError> {
61 let v: serde_json::Value = serde_json::from_str(encrypted_json)?;
62 let b64 = v["encryptedMessage"]
63 .as_str()
64 .ok_or_else(|| MessageBoxError::Encryption("missing encryptedMessage field".to_string()))?;
65
66 let ciphertext = STANDARD
67 .decode(b64)
68 .map_err(|e| MessageBoxError::Encryption(format!("base64 decode: {e}")))?;
69
70 let pk = PublicKey::from_string(sender_pubkey_hex)
71 .map_err(|e| MessageBoxError::Encryption(e.to_string()))?;
72
73 let result = wallet
74 .decrypt(
75 DecryptArgs {
76 protocol_id: Protocol {
77 security_level: 1,
78 protocol: "messagebox".to_string(),
79 },
80 key_id: "1".to_string(),
81 counterparty: Counterparty {
82 counterparty_type: CounterpartyType::Other,
83 public_key: Some(pk),
84 },
85 ciphertext,
86 privileged: false,
87 privileged_reason: None,
88 seek_permission: None,
89 },
90 originator,
91 )
92 .await
93 .map_err(|e| MessageBoxError::Wallet(e.to_string()))?;
94
95 String::from_utf8(result.plaintext)
96 .map_err(|e| MessageBoxError::Encryption(format!("utf-8 decode: {e}")))
97}
98
99pub async fn try_decrypt_message<W: WalletInterface>(
111 wallet: &W,
112 raw_body: &str,
113 sender_pubkey_hex: &str,
114 originator: Option<&str>,
115) -> String {
116 if let Ok(v) = serde_json::from_str::<serde_json::Value>(raw_body) {
118 if let Some(inner) = v.get("message") {
120 let inner_str = if inner.is_string() {
121 inner.as_str().unwrap().to_string()
122 } else {
123 inner.to_string()
124 };
125 return Box::pin(try_decrypt_message(wallet, &inner_str, sender_pubkey_hex, originator)).await;
127 }
128
129 if v.get("encryptedMessage").is_some() {
131 return decrypt_body(wallet, raw_body, sender_pubkey_hex, originator)
132 .await
133 .unwrap_or_else(|_| raw_body.to_string());
134 }
135 }
136
137 raw_body.to_string()
139}
140
141pub async fn generate_message_id<W: WalletInterface>(
148 wallet: &W,
149 body: &str,
150 recipient_pubkey_hex: &str,
151 originator: Option<&str>,
152) -> Result<String, MessageBoxError> {
153 let json_body = serde_json::to_string(body)?;
155
156 let pk = PublicKey::from_string(recipient_pubkey_hex)
157 .map_err(|e| MessageBoxError::Encryption(e.to_string()))?;
158
159 let result = wallet
160 .create_hmac(
161 CreateHmacArgs {
162 protocol_id: Protocol {
163 security_level: 1,
164 protocol: "messagebox".to_string(),
165 },
166 key_id: "1".to_string(),
167 counterparty: Counterparty {
168 counterparty_type: CounterpartyType::Other,
169 public_key: Some(pk),
170 },
171 data: json_body.as_bytes().to_vec(),
172 privileged: false,
173 privileged_reason: None,
174 seek_permission: None,
175 },
176 originator,
177 )
178 .await
179 .map_err(|e| MessageBoxError::Wallet(e.to_string()))?;
180
181 Ok(result.hmac.iter().map(|b| format!("{b:02x}")).collect())
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188 use bsv::primitives::private_key::PrivateKey;
189 use bsv::wallet::interfaces::GetPublicKeyArgs;
190 use bsv::wallet::proto_wallet::ProtoWallet;
191
192 fn make_wallet() -> ProtoWallet {
193 let key = PrivateKey::from_random().expect("random key");
194 ProtoWallet::new(key)
195 }
196
197 async fn identity_hex(wallet: &ProtoWallet) -> String {
198 wallet
199 .get_public_key(
200 GetPublicKeyArgs {
201 identity_key: true,
202 protocol_id: None,
203 key_id: None,
204 counterparty: None,
205 privileged: false,
206 privileged_reason: None,
207 for_self: None,
208 seek_permission: None,
209 },
210 None,
211 )
212 .await
213 .expect("get_public_key")
214 .public_key
215 .to_der_hex()
216 }
217
218 #[tokio::test]
219 async fn encrypt_body_produces_valid_json_with_base64() {
220 let sender = make_wallet();
221 let receiver = make_wallet();
222 let receiver_pk = identity_hex(&receiver).await;
223
224 let encrypted = encrypt_body(&sender, "hello world", &receiver_pk, None)
225 .await
226 .expect("encrypt_body");
227
228 let v: serde_json::Value = serde_json::from_str(&encrypted).expect("valid json");
229 let b64 = v["encryptedMessage"].as_str().expect("encryptedMessage field");
230 STANDARD.decode(b64).expect("valid STANDARD base64");
232 }
233
234 #[tokio::test]
235 async fn encrypt_decrypt_round_trip() {
236 let sender = make_wallet();
237 let receiver = make_wallet();
238 let sender_pk = identity_hex(&sender).await;
239 let receiver_pk = identity_hex(&receiver).await;
240
241 let message = "The quick brown fox jumps over the lazy dog — unicode: 日本語 🦊";
242 let encrypted = encrypt_body(&sender, message, &receiver_pk, None)
243 .await
244 .expect("encrypt");
245 let decrypted = decrypt_body(&receiver, &encrypted, &sender_pk, None)
246 .await
247 .expect("decrypt");
248
249 assert_eq!(decrypted, message);
250 }
251
252 #[tokio::test]
253 async fn try_decrypt_plaintext_passthrough() {
254 let wallet = make_wallet();
255 let other = make_wallet();
256 let other_pk = identity_hex(&other).await;
257
258 let result = try_decrypt_message(&wallet, "plain text body", &other_pk, None).await;
259 assert_eq!(result, "plain text body");
260 }
261
262 #[tokio::test]
263 async fn try_decrypt_json_without_encrypted_message_passthrough() {
264 let wallet = make_wallet();
265 let other = make_wallet();
266 let other_pk = identity_hex(&other).await;
267
268 let input = r#"{"foo":"bar","baz":42}"#;
269 let result = try_decrypt_message(&wallet, input, &other_pk, None).await;
270 assert_eq!(result, input);
271 }
272
273 #[tokio::test]
274 async fn try_decrypt_with_encrypted_body_decrypts() {
275 let sender = make_wallet();
276 let receiver = make_wallet();
277 let sender_pk = identity_hex(&sender).await;
278 let receiver_pk = identity_hex(&receiver).await;
279
280 let message = "secret message content";
281 let encrypted = encrypt_body(&sender, message, &receiver_pk, None)
282 .await
283 .expect("encrypt");
284
285 let result = try_decrypt_message(&receiver, &encrypted, &sender_pk, None).await;
286 assert_eq!(result, message);
287 }
288
289 #[tokio::test]
290 async fn try_decrypt_unwraps_payment_envelope_and_decrypts() {
291 let sender = make_wallet();
292 let receiver = make_wallet();
293 let sender_pk = identity_hex(&sender).await;
294 let receiver_pk = identity_hex(&receiver).await;
295
296 let message = "payment wrapped message";
297 let encrypted = encrypt_body(&sender, message, &receiver_pk, None)
298 .await
299 .expect("encrypt");
300
301 let wrapped = serde_json::json!({
303 "message": encrypted,
304 "payment": {"txid": "abc123"}
305 })
306 .to_string();
307
308 let result = try_decrypt_message(&receiver, &wrapped, &sender_pk, None).await;
309 assert_eq!(result, message);
310 }
311
312 #[tokio::test]
313 async fn generate_message_id_returns_64_char_hex() {
314 let wallet = make_wallet();
315 let other = make_wallet();
316 let other_pk = identity_hex(&other).await;
317
318 let id = generate_message_id(&wallet, "test body", &other_pk, None)
319 .await
320 .expect("generate_message_id");
321
322 assert_eq!(id.len(), 64, "HMAC hex should be 64 chars (32 bytes)");
323 assert!(id.chars().all(|c| c.is_ascii_hexdigit()), "all hex chars");
324 assert!(id.chars().all(|c| !c.is_uppercase()), "lowercase hex");
325 }
326
327 #[tokio::test]
328 async fn generate_message_id_is_deterministic() {
329 let wallet = make_wallet();
330 let other = make_wallet();
331 let other_pk = identity_hex(&other).await;
332
333 let id1 = generate_message_id(&wallet, "same body", &other_pk, None)
334 .await
335 .expect("first call");
336 let id2 = generate_message_id(&wallet, "same body", &other_pk, None)
337 .await
338 .expect("second call");
339
340 assert_eq!(id1, id2, "same inputs must produce same HMAC");
341 }
342}