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
141#[derive(Clone, Debug, PartialEq, Eq)]
153pub enum DecryptOutcome {
154 Decrypted(String),
158 Plaintext(String),
163}
164
165impl DecryptOutcome {
166 pub fn into_body(self) -> String {
169 match self {
170 DecryptOutcome::Decrypted(s) | DecryptOutcome::Plaintext(s) => s,
171 }
172 }
173
174 pub fn is_authenticated(&self) -> bool {
176 matches!(self, DecryptOutcome::Decrypted(_))
177 }
178}
179
180pub async fn try_decrypt_message_typed<W: WalletInterface>(
192 wallet: &W,
193 raw_body: &str,
194 sender_pubkey_hex: &str,
195 originator: Option<&str>,
196) -> DecryptOutcome {
197 if let Ok(v) = serde_json::from_str::<serde_json::Value>(raw_body) {
198 if let Some(inner) = v.get("message") {
201 let inner_str = if inner.is_string() {
202 inner.as_str().unwrap().to_string()
203 } else {
204 inner.to_string()
205 };
206 return Box::pin(try_decrypt_message_typed(
207 wallet,
208 &inner_str,
209 sender_pubkey_hex,
210 originator,
211 ))
212 .await;
213 }
214
215 if v.get("encryptedMessage").is_some() {
217 return match decrypt_body(wallet, raw_body, sender_pubkey_hex, originator).await {
218 Ok(plaintext) => DecryptOutcome::Decrypted(plaintext),
219 Err(_) => DecryptOutcome::Plaintext(raw_body.to_string()),
220 };
221 }
222 }
223
224 DecryptOutcome::Plaintext(raw_body.to_string())
226}
227
228pub async fn generate_message_id<W: WalletInterface>(
235 wallet: &W,
236 body: &str,
237 recipient_pubkey_hex: &str,
238 originator: Option<&str>,
239) -> Result<String, MessageBoxError> {
240 let json_body = serde_json::to_string(body)?;
242
243 let pk = PublicKey::from_string(recipient_pubkey_hex)
244 .map_err(|e| MessageBoxError::Encryption(e.to_string()))?;
245
246 let result = wallet
247 .create_hmac(
248 CreateHmacArgs {
249 protocol_id: Protocol {
250 security_level: 1,
251 protocol: "messagebox".to_string(),
252 },
253 key_id: "1".to_string(),
254 counterparty: Counterparty {
255 counterparty_type: CounterpartyType::Other,
256 public_key: Some(pk),
257 },
258 data: json_body.as_bytes().to_vec(),
259 privileged: false,
260 privileged_reason: None,
261 seek_permission: None,
262 },
263 originator,
264 )
265 .await
266 .map_err(|e| MessageBoxError::Wallet(e.to_string()))?;
267
268 Ok(result.hmac.iter().map(|b| format!("{b:02x}")).collect())
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275 use bsv::primitives::private_key::PrivateKey;
276 use bsv::wallet::interfaces::GetPublicKeyArgs;
277 use bsv::wallet::proto_wallet::ProtoWallet;
278
279 fn make_wallet() -> ProtoWallet {
280 let key = PrivateKey::from_random().expect("random key");
281 ProtoWallet::new(key)
282 }
283
284 async fn identity_hex(wallet: &ProtoWallet) -> String {
285 wallet
286 .get_public_key(
287 GetPublicKeyArgs {
288 identity_key: true,
289 protocol_id: None,
290 key_id: None,
291 counterparty: None,
292 privileged: false,
293 privileged_reason: None,
294 for_self: None,
295 seek_permission: None,
296 },
297 None,
298 )
299 .await
300 .expect("get_public_key")
301 .public_key
302 .to_der_hex()
303 }
304
305 #[tokio::test]
306 async fn encrypt_body_produces_valid_json_with_base64() {
307 let sender = make_wallet();
308 let receiver = make_wallet();
309 let receiver_pk = identity_hex(&receiver).await;
310
311 let encrypted = encrypt_body(&sender, "hello world", &receiver_pk, None)
312 .await
313 .expect("encrypt_body");
314
315 let v: serde_json::Value = serde_json::from_str(&encrypted).expect("valid json");
316 let b64 = v["encryptedMessage"].as_str().expect("encryptedMessage field");
317 STANDARD.decode(b64).expect("valid STANDARD base64");
319 }
320
321 #[tokio::test]
322 async fn encrypt_decrypt_round_trip() {
323 let sender = make_wallet();
324 let receiver = make_wallet();
325 let sender_pk = identity_hex(&sender).await;
326 let receiver_pk = identity_hex(&receiver).await;
327
328 let message = "The quick brown fox jumps over the lazy dog — unicode: 日本語 🦊";
329 let encrypted = encrypt_body(&sender, message, &receiver_pk, None)
330 .await
331 .expect("encrypt");
332 let decrypted = decrypt_body(&receiver, &encrypted, &sender_pk, None)
333 .await
334 .expect("decrypt");
335
336 assert_eq!(decrypted, message);
337 }
338
339 #[tokio::test]
340 async fn try_decrypt_plaintext_passthrough() {
341 let wallet = make_wallet();
342 let other = make_wallet();
343 let other_pk = identity_hex(&other).await;
344
345 let result = try_decrypt_message(&wallet, "plain text body", &other_pk, None).await;
346 assert_eq!(result, "plain text body");
347 }
348
349 #[tokio::test]
350 async fn try_decrypt_json_without_encrypted_message_passthrough() {
351 let wallet = make_wallet();
352 let other = make_wallet();
353 let other_pk = identity_hex(&other).await;
354
355 let input = r#"{"foo":"bar","baz":42}"#;
356 let result = try_decrypt_message(&wallet, input, &other_pk, None).await;
357 assert_eq!(result, input);
358 }
359
360 #[tokio::test]
361 async fn try_decrypt_with_encrypted_body_decrypts() {
362 let sender = make_wallet();
363 let receiver = make_wallet();
364 let sender_pk = identity_hex(&sender).await;
365 let receiver_pk = identity_hex(&receiver).await;
366
367 let message = "secret message content";
368 let encrypted = encrypt_body(&sender, message, &receiver_pk, None)
369 .await
370 .expect("encrypt");
371
372 let result = try_decrypt_message(&receiver, &encrypted, &sender_pk, None).await;
373 assert_eq!(result, message);
374 }
375
376 #[tokio::test]
377 async fn try_decrypt_unwraps_payment_envelope_and_decrypts() {
378 let sender = make_wallet();
379 let receiver = make_wallet();
380 let sender_pk = identity_hex(&sender).await;
381 let receiver_pk = identity_hex(&receiver).await;
382
383 let message = "payment wrapped message";
384 let encrypted = encrypt_body(&sender, message, &receiver_pk, None)
385 .await
386 .expect("encrypt");
387
388 let wrapped = serde_json::json!({
390 "message": encrypted,
391 "payment": {"txid": "abc123"}
392 })
393 .to_string();
394
395 let result = try_decrypt_message(&receiver, &wrapped, &sender_pk, None).await;
396 assert_eq!(result, message);
397 }
398
399 #[tokio::test]
400 async fn typed_decrypt_plaintext_body_is_plaintext_outcome() {
401 let wallet = make_wallet();
404 let other = make_wallet();
405 let other_pk = identity_hex(&other).await;
406
407 let body = r#"{"sender":0,"is_broadcast":true,"msg":{"round":1}}"#;
408 let outcome = try_decrypt_message_typed(&wallet, body, &other_pk, None).await;
409 assert_eq!(outcome, DecryptOutcome::Plaintext(body.to_string()));
410 assert!(!outcome.is_authenticated());
411 assert_eq!(outcome.into_body(), body);
412 }
413
414 #[tokio::test]
415 async fn typed_decrypt_non_json_plaintext_is_plaintext_outcome() {
416 let wallet = make_wallet();
417 let other = make_wallet();
418 let other_pk = identity_hex(&other).await;
419
420 let outcome = try_decrypt_message_typed(&wallet, "plain text", &other_pk, None).await;
421 assert!(!outcome.is_authenticated());
422 }
423
424 #[tokio::test]
425 async fn typed_decrypt_failed_aead_fails_open_to_plaintext_outcome() {
426 let wallet = make_wallet();
429 let other = make_wallet();
430 let other_pk = identity_hex(&other).await;
431
432 let body = r#"{"encryptedMessage":"AAAAbase64ciphertext=="}"#;
433 let outcome = try_decrypt_message_typed(&wallet, body, &other_pk, None).await;
434 assert!(!outcome.is_authenticated(), "garbage ciphertext must not authenticate");
435 let legacy = try_decrypt_message(&wallet, body, &other_pk, None).await;
437 assert_eq!(outcome.into_body(), legacy);
438 }
439
440 #[tokio::test]
441 async fn typed_decrypt_round_trip_is_decrypted_outcome() {
442 let sender = make_wallet();
443 let receiver = make_wallet();
444 let sender_pk = identity_hex(&sender).await;
445 let receiver_pk = identity_hex(&receiver).await;
446
447 let message = "secret authenticated content";
448 let encrypted = encrypt_body(&sender, message, &receiver_pk, None)
449 .await
450 .expect("encrypt");
451
452 let outcome = try_decrypt_message_typed(&receiver, &encrypted, &sender_pk, None).await;
453 assert_eq!(outcome, DecryptOutcome::Decrypted(message.to_string()));
454 assert!(outcome.is_authenticated());
455 }
456
457 #[tokio::test]
458 async fn typed_decrypt_payment_envelope_round_trip_is_decrypted() {
459 let sender = make_wallet();
460 let receiver = make_wallet();
461 let sender_pk = identity_hex(&sender).await;
462 let receiver_pk = identity_hex(&receiver).await;
463
464 let message = "wrapped authenticated content";
465 let encrypted = encrypt_body(&sender, message, &receiver_pk, None)
466 .await
467 .expect("encrypt");
468 let wrapped = serde_json::json!({"message": encrypted, "payment": {"txid": "abc"}})
469 .to_string();
470
471 let outcome = try_decrypt_message_typed(&receiver, &wrapped, &sender_pk, None).await;
472 assert_eq!(outcome, DecryptOutcome::Decrypted(message.to_string()));
473 }
474
475 #[tokio::test]
476 async fn generate_message_id_returns_64_char_hex() {
477 let wallet = make_wallet();
478 let other = make_wallet();
479 let other_pk = identity_hex(&other).await;
480
481 let id = generate_message_id(&wallet, "test body", &other_pk, None)
482 .await
483 .expect("generate_message_id");
484
485 assert_eq!(id.len(), 64, "HMAC hex should be 64 chars (32 bytes)");
486 assert!(id.chars().all(|c| c.is_ascii_hexdigit()), "all hex chars");
487 assert!(id.chars().all(|c| !c.is_uppercase()), "lowercase hex");
488 }
489
490 #[tokio::test]
491 async fn generate_message_id_is_deterministic() {
492 let wallet = make_wallet();
493 let other = make_wallet();
494 let other_pk = identity_hex(&other).await;
495
496 let id1 = generate_message_id(&wallet, "same body", &other_pk, None)
497 .await
498 .expect("first call");
499 let id2 = generate_message_id(&wallet, "same body", &other_pk, None)
500 .await
501 .expect("second call");
502
503 assert_eq!(id1, id2, "same inputs must produce same HMAC");
504 }
505}