1use std::str::FromStr;
26
27use crate::message::{Action, Message, Payload};
28use crate::prelude::{CantDoReason, MostroError, ServiceError};
29use nostr::nips::nip44;
30use nostr::nips::nip59::GiftWrapSealBuilder;
31use nostr_sdk::prelude::*;
32
33const RANGE_RANDOM_TIMESTAMP_TWEAK_SECS: u64 = 172_800;
36
37#[derive(Debug, Clone)]
39pub struct WrapOptions {
40 pub pow: u8,
42 pub expiration: Option<Timestamp>,
44 pub signed: bool,
49}
50
51impl Default for WrapOptions {
52 fn default() -> Self {
53 Self {
54 pow: 0,
55 expiration: None,
56 signed: true,
57 }
58 }
59}
60
61#[derive(Debug, Clone)]
64pub struct UnwrappedMessage {
65 pub message: Message,
67 pub signature: Option<Signature>,
70 pub sender: PublicKey,
72 pub identity: PublicKey,
76 pub created_at: Timestamp,
78}
79
80pub async fn wrap_message(
94 message: &Message,
95 identity_keys: &Keys,
96 trade_keys: &Keys,
97 receiver: PublicKey,
98 opts: WrapOptions,
99) -> Result<Event, MostroError> {
100 let message_json = message.as_json().map_err(MostroError::MostroInternalErr)?;
101
102 let content = if opts.signed {
103 let sig = Message::sign(message_json, trade_keys);
104 serde_json::to_string(&(message, Some(sig.to_string())))
105 .map_err(|_| MostroError::MostroInternalErr(ServiceError::MessageSerializationError))?
106 } else {
107 serde_json::to_string(&(message, Option::<String>::None))
108 .map_err(|_| MostroError::MostroInternalErr(ServiceError::MessageSerializationError))?
109 };
110
111 let rumor =
115 EventBuilder::new(Kind::TextNote, content).finalize_unsigned(trade_keys.public_key());
116
117 let seal: Event = GiftWrapSealBuilder::new(rumor, receiver)
122 .finalize(identity_keys)
123 .map_err(|e| MostroError::MostroInternalErr(ServiceError::NostrError(e.to_string())))?;
124
125 gift_wrap_from_seal_with_pow(&seal, receiver, opts.pow, opts.expiration)
126}
127
128fn gift_wrap_from_seal_with_pow(
136 seal: &Event,
137 receiver: PublicKey,
138 pow: u8,
139 expiration: Option<Timestamp>,
140) -> Result<Event, MostroError> {
141 if seal.kind != Kind::Seal {
142 return Err(MostroError::MostroInternalErr(
143 ServiceError::UnexpectedError("expected Seal kind".to_string()),
144 ));
145 }
146
147 let ephemeral = Keys::generate();
148 let encrypted = nip44::encrypt(
149 ephemeral.secret_key(),
150 &receiver,
151 seal.as_json(),
152 nip44::Version::default(),
153 )
154 .map_err(|e| MostroError::MostroInternalErr(ServiceError::EncryptionError(e.to_string())))?;
155
156 let mut tags: Vec<Tag> = Vec::new();
157 if let Some(exp) = expiration {
158 tags.push(Tag::expiration(exp));
159 }
160 tags.push(Tag::public_key(receiver));
161
162 let unsigned = EventBuilder::new(Kind::GiftWrap, encrypted)
163 .tags(tags)
164 .custom_created_at(tweaked_timestamp())
165 .finalize_unsigned(ephemeral.public_key());
166
167 let unsigned = match core::num::NonZeroU8::new(pow) {
168 Some(pow) => unsigned
169 .mine(&SingleThreadPow, pow)
170 .map_err(|e| MostroError::MostroInternalErr(ServiceError::NostrError(e.to_string())))?,
171 None => unsigned,
172 };
173
174 unsigned
175 .finalize(&ephemeral)
176 .map_err(|e| MostroError::MostroInternalErr(ServiceError::NostrError(e.to_string())))
177}
178
179fn tweaked_timestamp() -> Timestamp {
181 let now = Timestamp::now().as_secs();
182 let entropy = Keys::generate();
183 let bytes = entropy.secret_key().to_secret_bytes();
184 let tweak = u64::from_le_bytes(bytes[0..8].try_into().expect("8 bytes"))
185 % RANGE_RANDOM_TIMESTAMP_TWEAK_SECS;
186 Timestamp::from_secs(now.saturating_sub(tweak))
187}
188
189pub async fn unwrap_message(
202 event: &Event,
203 receiver_keys: &Keys,
204) -> Result<Option<UnwrappedMessage>, MostroError> {
205 if event.kind != Kind::GiftWrap {
206 return Err(MostroError::MostroInternalErr(
207 ServiceError::UnexpectedError("event is not a GiftWrap".to_string()),
208 ));
209 }
210
211 let seal_json = match nip44::decrypt(receiver_keys.secret_key(), &event.pubkey, &event.content)
214 {
215 Ok(s) => s,
216 Err(_) => return Ok(None),
217 };
218
219 let seal: Event = Event::from_json(&seal_json).map_err(|e| {
220 MostroError::MostroInternalErr(ServiceError::NostrError(format!(
221 "malformed seal JSON: {e}"
222 )))
223 })?;
224
225 if seal.kind != Kind::Seal {
226 return Err(MostroError::MostroInternalErr(
227 ServiceError::UnexpectedError("inner event is not a Seal".to_string()),
228 ));
229 }
230
231 seal.verify_signature().then_some(()).ok_or_else(|| {
232 MostroError::MostroInternalErr(ServiceError::NostrError(
233 "invalid seal signature".to_string(),
234 ))
235 })?;
236
237 let rumor_json = nip44::decrypt(receiver_keys.secret_key(), &seal.pubkey, &seal.content)
241 .map_err(|e| {
242 MostroError::MostroInternalErr(ServiceError::DecryptionError(e.to_string()))
243 })?;
244
245 let rumor: UnsignedEvent = UnsignedEvent::from_json(&rumor_json).map_err(|e| {
246 MostroError::MostroInternalErr(ServiceError::NostrError(format!(
247 "malformed rumor JSON: {e}"
248 )))
249 })?;
250
251 if rumor.kind != Kind::TextNote {
252 return Err(MostroError::MostroInternalErr(
253 ServiceError::UnexpectedError("rumor is not a TextNote".to_string()),
254 ));
255 }
256
257 let (message, sig_str): (Message, Option<String>) = serde_json::from_str(&rumor.content)
258 .map_err(|_| MostroError::MostroInternalErr(ServiceError::MessageSerializationError))?;
259
260 let signature = match sig_str {
261 Some(s) => {
262 let sig = Signature::from_str(&s).map_err(|e| {
263 MostroError::MostroInternalErr(ServiceError::UnexpectedError(format!(
264 "malformed rumor signature: {e}"
265 )))
266 })?;
267 let message_json = message.as_json().map_err(MostroError::MostroInternalErr)?;
268 if !Message::verify_signature(message_json, rumor.pubkey, sig) {
269 return Err(MostroError::MostroInternalErr(
270 ServiceError::UnexpectedError(
271 "rumor signature does not verify against sender".to_string(),
272 ),
273 ));
274 }
275 Some(sig)
276 }
277 None => None,
278 };
279
280 Ok(Some(UnwrappedMessage {
281 message,
282 signature,
283 sender: rumor.pubkey,
284 identity: seal.pubkey,
285 created_at: rumor.created_at,
286 }))
287}
288
289pub fn validate_response(
302 message: &Message,
303 expected_request_id: Option<u64>,
304) -> Result<(), MostroError> {
305 let inner = message.get_inner_message_kind();
306
307 if let Some(Payload::CantDo(reason)) = &inner.payload {
308 return Err(MostroError::MostroCantDo(
309 reason.clone().unwrap_or(CantDoReason::InvalidAction),
310 ));
311 }
312
313 if let Some(expected) = expected_request_id {
314 match inner.request_id {
315 Some(got) if got == expected => {}
316 Some(_) => {
317 return Err(MostroError::MostroInternalErr(
318 ServiceError::UnexpectedError("mismatched request_id".to_string()),
319 ));
320 }
321 None => {
322 if !action_accepts_missing_request_id(&inner.action) {
323 return Err(MostroError::MostroInternalErr(
324 ServiceError::UnexpectedError(
325 "missing request_id on a response that requires one".to_string(),
326 ),
327 ));
328 }
329 }
330 }
331 }
332
333 Ok(())
334}
335
336fn action_accepts_missing_request_id(action: &Action) -> bool {
339 matches!(
340 action,
341 Action::BuyerTookOrder
342 | Action::HoldInvoicePaymentAccepted
343 | Action::HoldInvoicePaymentSettled
344 | Action::HoldInvoicePaymentCanceled
345 | Action::WaitingSellerToPay
346 | Action::WaitingBuyerInvoice
347 | Action::BuyerInvoiceAccepted
348 | Action::PurchaseCompleted
349 | Action::Released
350 | Action::FiatSentOk
351 | Action::Canceled
352 | Action::CooperativeCancelInitiatedByPeer
353 | Action::CooperativeCancelAccepted
354 | Action::DisputeInitiatedByPeer
355 | Action::AdminSettled
356 | Action::AdminCanceled
357 | Action::AdminTookDispute
358 | Action::PaymentFailed
359 | Action::InvoiceUpdated
360 | Action::Rate
361 | Action::RateReceived
362 | Action::SendDm
363 | Action::BondSlashed
364 | Action::CashuEscrowLocked
365 | Action::CashuPmSignature
366 )
367}
368
369#[cfg(test)]
370mod tests {
371 use super::*;
372 use crate::message::{Action, MessageKind, Payload};
373 use uuid::uuid;
374
375 fn sample_order_message(request_id: Option<u64>) -> Message {
376 let peer = crate::message::Peer::new(
377 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
378 None,
379 );
380 Message::Order(MessageKind::new(
381 Some(uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23")),
382 request_id,
383 Some(1),
384 Action::FiatSentOk,
385 Some(Payload::Peer(peer)),
386 ))
387 }
388
389 #[tokio::test]
390 async fn wrap_then_unwrap_roundtrip() {
391 let identity_keys = Keys::generate();
392 let trade_keys = Keys::generate();
393 let receiver_keys = Keys::generate();
394
395 let message = sample_order_message(Some(42));
396
397 let wrapped = wrap_message(
398 &message,
399 &identity_keys,
400 &trade_keys,
401 receiver_keys.public_key(),
402 WrapOptions::default(),
403 )
404 .await
405 .expect("wrap");
406
407 assert_eq!(wrapped.kind, Kind::GiftWrap);
408 assert!(wrapped
409 .tags
410 .iter()
411 .any(|t| t.as_slice().first().map(|s| s.as_str()) == Some("p")));
412
413 let unwrapped = unwrap_message(&wrapped, &receiver_keys)
414 .await
415 .expect("unwrap result")
416 .expect("unwrap some");
417
418 assert_eq!(unwrapped.sender, trade_keys.public_key());
419 assert_eq!(unwrapped.identity, identity_keys.public_key());
420 assert_eq!(
421 unwrapped.message.as_json().unwrap(),
422 message.as_json().unwrap()
423 );
424 assert!(unwrapped.signature.is_some());
425 }
426
427 #[tokio::test]
428 async fn full_privacy_mode_identity_equals_sender() {
429 let trade_keys = Keys::generate();
431 let receiver_keys = Keys::generate();
432
433 let wrapped = wrap_message(
434 &sample_order_message(Some(1)),
435 &trade_keys,
436 &trade_keys,
437 receiver_keys.public_key(),
438 WrapOptions::default(),
439 )
440 .await
441 .expect("wrap");
442
443 let unwrapped = unwrap_message(&wrapped, &receiver_keys)
444 .await
445 .expect("unwrap")
446 .expect("some");
447
448 assert_eq!(unwrapped.sender, trade_keys.public_key());
449 assert_eq!(unwrapped.identity, trade_keys.public_key());
450 }
451
452 #[tokio::test]
453 async fn unwrap_with_corrupted_seal_returns_err() {
454 let receiver_keys = Keys::generate();
455 let ephemeral = Keys::generate();
456
457 let encrypted = nip44::encrypt(
461 ephemeral.secret_key(),
462 &receiver_keys.public_key(),
463 "not a seal",
464 nip44::Version::default(),
465 )
466 .expect("encrypt");
467
468 let corrupted = EventBuilder::new(Kind::GiftWrap, encrypted)
469 .tags([Tag::public_key(receiver_keys.public_key())])
470 .finalize(&ephemeral)
471 .expect("sign");
472
473 let result = unwrap_message(&corrupted, &receiver_keys).await;
474 assert!(
475 matches!(result, Err(MostroError::MostroInternalErr(_))),
476 "expected Err for corrupted gift wrap, got {result:?}",
477 );
478 }
479
480 async fn wrap_with_raw_inner(
484 identity_keys: &Keys,
485 trade_keys: &Keys,
486 receiver: PublicKey,
487 inner: (&Message, Option<String>),
488 ) -> Event {
489 let content = serde_json::to_string(&inner).unwrap();
490 let rumor =
491 EventBuilder::new(Kind::TextNote, content).finalize_unsigned(trade_keys.public_key());
492 let seal = GiftWrapSealBuilder::new(rumor, receiver)
493 .finalize(identity_keys)
494 .unwrap();
495 gift_wrap_from_seal_with_pow(&seal, receiver, 0, None).unwrap()
496 }
497
498 #[tokio::test]
499 async fn unwrap_with_malformed_signature_errors() {
500 let identity_keys = Keys::generate();
501 let trade_keys = Keys::generate();
502 let receiver_keys = Keys::generate();
503 let msg = sample_order_message(Some(1));
504
505 let wrapped = wrap_with_raw_inner(
506 &identity_keys,
507 &trade_keys,
508 receiver_keys.public_key(),
509 (&msg, Some("not-a-hex-signature".to_string())),
510 )
511 .await;
512
513 let result = unwrap_message(&wrapped, &receiver_keys).await;
514 assert!(
515 matches!(result, Err(MostroError::MostroInternalErr(_))),
516 "malformed signature must surface as Err, got {result:?}",
517 );
518 }
519
520 #[tokio::test]
521 async fn unwrap_with_signature_for_other_content_errors() {
522 let identity_keys = Keys::generate();
523 let trade_keys = Keys::generate();
524 let receiver_keys = Keys::generate();
525 let msg = sample_order_message(Some(1));
526 let bogus = Message::sign("not the real message".to_string(), &trade_keys);
528
529 let wrapped = wrap_with_raw_inner(
530 &identity_keys,
531 &trade_keys,
532 receiver_keys.public_key(),
533 (&msg, Some(bogus.to_string())),
534 )
535 .await;
536
537 let result = unwrap_message(&wrapped, &receiver_keys).await;
538 assert!(
539 matches!(result, Err(MostroError::MostroInternalErr(_))),
540 "non-verifying signature must surface as Err, got {result:?}",
541 );
542 }
543
544 #[tokio::test]
545 async fn unwrap_with_wrong_receiver_keys_returns_none() {
546 let identity_keys = Keys::generate();
547 let trade_keys = Keys::generate();
548 let receiver_keys = Keys::generate();
549 let stranger_keys = Keys::generate();
550
551 let wrapped = wrap_message(
552 &sample_order_message(Some(1)),
553 &identity_keys,
554 &trade_keys,
555 receiver_keys.public_key(),
556 WrapOptions::default(),
557 )
558 .await
559 .expect("wrap");
560
561 let result = unwrap_message(&wrapped, &stranger_keys)
562 .await
563 .expect("call should not error");
564 assert!(result.is_none());
565 }
566
567 #[tokio::test]
568 async fn signature_is_verifiable_with_trade_pubkey() {
569 let identity_keys = Keys::generate();
570 let trade_keys = Keys::generate();
571 let receiver_keys = Keys::generate();
572 let message = sample_order_message(Some(7));
573
574 let wrapped = wrap_message(
575 &message,
576 &identity_keys,
577 &trade_keys,
578 receiver_keys.public_key(),
579 WrapOptions::default(),
580 )
581 .await
582 .unwrap();
583
584 let unwrapped = unwrap_message(&wrapped, &receiver_keys)
585 .await
586 .unwrap()
587 .unwrap();
588
589 let sig = unwrapped.signature.expect("signed");
590 let json = unwrapped.message.as_json().unwrap();
591 assert!(Message::verify_signature(
592 json,
593 trade_keys.public_key(),
594 sig
595 ));
596 }
597
598 #[tokio::test]
599 async fn unsigned_wrap_has_no_signature() {
600 let identity_keys = Keys::generate();
601 let trade_keys = Keys::generate();
602 let receiver_keys = Keys::generate();
603
604 let wrapped = wrap_message(
605 &sample_order_message(Some(3)),
606 &identity_keys,
607 &trade_keys,
608 receiver_keys.public_key(),
609 WrapOptions {
610 signed: false,
611 ..WrapOptions::default()
612 },
613 )
614 .await
615 .expect("wrap");
616
617 let unwrapped = unwrap_message(&wrapped, &receiver_keys)
618 .await
619 .unwrap()
620 .unwrap();
621 assert!(unwrapped.signature.is_none());
622 }
623
624 #[tokio::test]
625 async fn expiration_tag_is_set_when_provided() {
626 let identity_keys = Keys::generate();
627 let trade_keys = Keys::generate();
628 let receiver_keys = Keys::generate();
629 let exp = Timestamp::from_secs(Timestamp::now().as_secs() + 3600);
630
631 let wrapped = wrap_message(
632 &sample_order_message(Some(1)),
633 &identity_keys,
634 &trade_keys,
635 receiver_keys.public_key(),
636 WrapOptions {
637 expiration: Some(exp),
638 ..WrapOptions::default()
639 },
640 )
641 .await
642 .expect("wrap");
643
644 let has_expiration = wrapped
645 .tags
646 .iter()
647 .any(|t| t.as_slice().first().map(|s| s.as_str()) == Some("expiration"));
648 assert!(has_expiration);
649 }
650
651 #[test]
652 fn validate_response_cant_do_short_circuits() {
653 let msg = Message::cant_do(
654 Some(uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23")),
655 Some(5),
656 Some(Payload::CantDo(Some(CantDoReason::NotAuthorized))),
657 );
658 let err = validate_response(&msg, Some(5)).unwrap_err();
659 match err {
660 MostroError::MostroCantDo(CantDoReason::NotAuthorized) => {}
661 _ => panic!("expected CantDo(NotAuthorized)"),
662 }
663 }
664
665 #[test]
666 fn validate_response_request_id_match() {
667 let msg = sample_order_message(Some(9));
668 validate_response(&msg, Some(9)).unwrap();
669 }
670
671 #[test]
672 fn validate_response_request_id_mismatch_errors() {
673 let msg = sample_order_message(Some(9));
674 let err = validate_response(&msg, Some(10)).unwrap_err();
675 assert!(matches!(err, MostroError::MostroInternalErr(_)));
676 }
677
678 #[test]
679 fn validate_response_allows_unsolicited_actions_without_request_id() {
680 let msg = Message::Order(MessageKind::new(
681 Some(uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23")),
682 None,
683 None,
684 Action::BuyerTookOrder,
685 None,
686 ));
687 validate_response(&msg, Some(1)).unwrap();
688 }
689
690 #[test]
691 fn validate_response_with_no_expected_id_is_ok() {
692 let msg = sample_order_message(None);
693 validate_response(&msg, None).unwrap();
694 }
695
696 #[test]
697 fn validate_response_allows_cashu_server_events_without_request_id() {
698 for action in [Action::CashuEscrowLocked, Action::CashuPmSignature] {
701 let msg = Message::Order(MessageKind::new(
702 Some(uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23")),
703 None,
704 None,
705 action,
706 None,
707 ));
708 validate_response(&msg, Some(1)).unwrap();
709 }
710 }
711}