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 mut rumor =
115 EventBuilder::new(Kind::TextNote, content).finalize_unsigned(trade_keys.public_key());
116
117 rumor.ensure_id();
122
123 let seal: Event = GiftWrapSealBuilder::new(rumor, receiver)
128 .finalize(identity_keys)
129 .map_err(|e| MostroError::MostroInternalErr(ServiceError::NostrError(e.to_string())))?;
130
131 gift_wrap_from_seal_with_pow(&seal, receiver, opts.pow, opts.expiration)
132}
133
134fn gift_wrap_from_seal_with_pow(
142 seal: &Event,
143 receiver: PublicKey,
144 pow: u8,
145 expiration: Option<Timestamp>,
146) -> Result<Event, MostroError> {
147 if seal.kind != Kind::Seal {
148 return Err(MostroError::MostroInternalErr(
149 ServiceError::UnexpectedError("expected Seal kind".to_string()),
150 ));
151 }
152
153 let ephemeral = Keys::generate();
154 let encrypted = nip44::encrypt(
155 ephemeral.secret_key(),
156 &receiver,
157 seal.as_json(),
158 nip44::Version::default(),
159 )
160 .map_err(|e| MostroError::MostroInternalErr(ServiceError::EncryptionError(e.to_string())))?;
161
162 let mut tags: Vec<Tag> = Vec::new();
163 if let Some(exp) = expiration {
164 tags.push(Tag::expiration(exp));
165 }
166 tags.push(Tag::public_key(receiver));
167
168 let unsigned = EventBuilder::new(Kind::GiftWrap, encrypted)
169 .tags(tags)
170 .custom_created_at(tweaked_timestamp())
171 .finalize_unsigned(ephemeral.public_key());
172
173 let unsigned = match core::num::NonZeroU8::new(pow) {
174 Some(pow) => unsigned
175 .mine(&SingleThreadPow, pow)
176 .map_err(|e| MostroError::MostroInternalErr(ServiceError::NostrError(e.to_string())))?,
177 None => unsigned,
178 };
179
180 unsigned
181 .finalize(&ephemeral)
182 .map_err(|e| MostroError::MostroInternalErr(ServiceError::NostrError(e.to_string())))
183}
184
185fn tweaked_timestamp() -> Timestamp {
187 let now = Timestamp::now().as_secs();
188 let entropy = Keys::generate();
189 let bytes = entropy.secret_key().to_secret_bytes();
190 let tweak = u64::from_le_bytes(bytes[0..8].try_into().expect("8 bytes"))
191 % RANGE_RANDOM_TIMESTAMP_TWEAK_SECS;
192 Timestamp::from_secs(now.saturating_sub(tweak))
193}
194
195pub async fn unwrap_message(
208 event: &Event,
209 receiver_keys: &Keys,
210) -> Result<Option<UnwrappedMessage>, MostroError> {
211 if event.kind != Kind::GiftWrap {
212 return Err(MostroError::MostroInternalErr(
213 ServiceError::UnexpectedError("event is not a GiftWrap".to_string()),
214 ));
215 }
216
217 let seal_json = match nip44::decrypt(receiver_keys.secret_key(), &event.pubkey, &event.content)
220 {
221 Ok(s) => s,
222 Err(_) => return Ok(None),
223 };
224
225 let seal: Event = Event::from_json(&seal_json).map_err(|e| {
226 MostroError::MostroInternalErr(ServiceError::NostrError(format!(
227 "malformed seal JSON: {e}"
228 )))
229 })?;
230
231 if seal.kind != Kind::Seal {
232 return Err(MostroError::MostroInternalErr(
233 ServiceError::UnexpectedError("inner event is not a Seal".to_string()),
234 ));
235 }
236
237 seal.verify_signature().then_some(()).ok_or_else(|| {
238 MostroError::MostroInternalErr(ServiceError::NostrError(
239 "invalid seal signature".to_string(),
240 ))
241 })?;
242
243 let rumor_json = nip44::decrypt(receiver_keys.secret_key(), &seal.pubkey, &seal.content)
247 .map_err(|e| {
248 MostroError::MostroInternalErr(ServiceError::DecryptionError(e.to_string()))
249 })?;
250
251 let rumor: UnsignedEvent = UnsignedEvent::from_json(&rumor_json).map_err(|e| {
252 MostroError::MostroInternalErr(ServiceError::NostrError(format!(
253 "malformed rumor JSON: {e}"
254 )))
255 })?;
256
257 if rumor.kind != Kind::TextNote {
258 return Err(MostroError::MostroInternalErr(
259 ServiceError::UnexpectedError("rumor is not a TextNote".to_string()),
260 ));
261 }
262
263 let (message, sig_str): (Message, Option<String>) = serde_json::from_str(&rumor.content)
264 .map_err(|_| MostroError::MostroInternalErr(ServiceError::MessageSerializationError))?;
265
266 let signature = match sig_str {
267 Some(s) => {
268 let sig = Signature::from_str(&s).map_err(|e| {
269 MostroError::MostroInternalErr(ServiceError::UnexpectedError(format!(
270 "malformed rumor signature: {e}"
271 )))
272 })?;
273 let message_json = message.as_json().map_err(MostroError::MostroInternalErr)?;
274 if !Message::verify_signature(message_json, rumor.pubkey, sig) {
275 return Err(MostroError::MostroInternalErr(
276 ServiceError::UnexpectedError(
277 "rumor signature does not verify against sender".to_string(),
278 ),
279 ));
280 }
281 Some(sig)
282 }
283 None => None,
284 };
285
286 Ok(Some(UnwrappedMessage {
287 message,
288 signature,
289 sender: rumor.pubkey,
290 identity: seal.pubkey,
291 created_at: rumor.created_at,
292 }))
293}
294
295pub fn validate_response(
308 message: &Message,
309 expected_request_id: Option<u64>,
310) -> Result<(), MostroError> {
311 let inner = message.get_inner_message_kind();
312
313 if let Some(Payload::CantDo(reason)) = &inner.payload {
314 return Err(MostroError::MostroCantDo(
315 reason.clone().unwrap_or(CantDoReason::InvalidAction),
316 ));
317 }
318
319 if let Some(expected) = expected_request_id {
320 match inner.request_id {
321 Some(got) if got == expected => {}
322 Some(_) => {
323 return Err(MostroError::MostroInternalErr(
324 ServiceError::UnexpectedError("mismatched request_id".to_string()),
325 ));
326 }
327 None => {
328 if !action_accepts_missing_request_id(&inner.action) {
329 return Err(MostroError::MostroInternalErr(
330 ServiceError::UnexpectedError(
331 "missing request_id on a response that requires one".to_string(),
332 ),
333 ));
334 }
335 }
336 }
337 }
338
339 Ok(())
340}
341
342fn action_accepts_missing_request_id(action: &Action) -> bool {
345 matches!(
346 action,
347 Action::BuyerTookOrder
348 | Action::HoldInvoicePaymentAccepted
349 | Action::HoldInvoicePaymentSettled
350 | Action::HoldInvoicePaymentCanceled
351 | Action::WaitingSellerToPay
352 | Action::WaitingBuyerInvoice
353 | Action::BuyerInvoiceAccepted
354 | Action::PurchaseCompleted
355 | Action::Released
356 | Action::FiatSentOk
357 | Action::Canceled
358 | Action::CooperativeCancelInitiatedByPeer
359 | Action::CooperativeCancelAccepted
360 | Action::DisputeInitiatedByPeer
361 | Action::AdminSettled
362 | Action::AdminCanceled
363 | Action::AdminTookDispute
364 | Action::PaymentFailed
365 | Action::InvoiceUpdated
366 | Action::Rate
367 | Action::RateReceived
368 | Action::SendDm
369 | Action::BondSlashed
370 | Action::CashuEscrowLocked
371 | Action::CashuPmSignature
372 )
373}
374
375#[cfg(test)]
376mod tests {
377 use super::*;
378 use crate::message::{Action, MessageKind, Payload};
379 use uuid::uuid;
380
381 fn sample_order_message(request_id: Option<u64>) -> Message {
382 let peer = crate::message::Peer::new(
383 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
384 None,
385 );
386 Message::Order(MessageKind::new(
387 Some(uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23")),
388 request_id,
389 Some(1),
390 Action::FiatSentOk,
391 Some(Payload::Peer(peer)),
392 ))
393 }
394
395 #[tokio::test]
396 async fn wrap_then_unwrap_roundtrip() {
397 let identity_keys = Keys::generate();
398 let trade_keys = Keys::generate();
399 let receiver_keys = Keys::generate();
400
401 let message = sample_order_message(Some(42));
402
403 let wrapped = wrap_message(
404 &message,
405 &identity_keys,
406 &trade_keys,
407 receiver_keys.public_key(),
408 WrapOptions::default(),
409 )
410 .await
411 .expect("wrap");
412
413 assert_eq!(wrapped.kind, Kind::GiftWrap);
414 assert!(wrapped
415 .tags
416 .iter()
417 .any(|t| t.as_slice().first().map(|s| s.as_str()) == Some("p")));
418
419 let unwrapped = unwrap_message(&wrapped, &receiver_keys)
420 .await
421 .expect("unwrap result")
422 .expect("unwrap some");
423
424 assert_eq!(unwrapped.sender, trade_keys.public_key());
425 assert_eq!(unwrapped.identity, identity_keys.public_key());
426 assert_eq!(
427 unwrapped.message.as_json().unwrap(),
428 message.as_json().unwrap()
429 );
430 assert!(unwrapped.signature.is_some());
431 }
432
433 #[tokio::test]
437 async fn rumor_id_serialized_inside_wrap() {
438 let identity_keys = Keys::generate();
439 let trade_keys = Keys::generate();
440 let receiver_keys = Keys::generate();
441
442 let wrapped = wrap_message(
443 &sample_order_message(Some(42)),
444 &identity_keys,
445 &trade_keys,
446 receiver_keys.public_key(),
447 WrapOptions::default(),
448 )
449 .await
450 .expect("wrap");
451
452 let seal_json = nip44::decrypt(
454 receiver_keys.secret_key(),
455 &wrapped.pubkey,
456 &wrapped.content,
457 )
458 .expect("decrypt wrap");
459 let seal: Event = Event::from_json(&seal_json).expect("parse seal");
460 let rumor_json = nip44::decrypt(receiver_keys.secret_key(), &seal.pubkey, &seal.content)
461 .expect("decrypt seal");
462
463 let rumor: UnsignedEvent = UnsignedEvent::from_json(&rumor_json).expect("parse rumor");
464 assert_eq!(rumor.id, Some(rumor.compute_id()));
465 }
466
467 #[tokio::test]
468 async fn full_privacy_mode_identity_equals_sender() {
469 let trade_keys = Keys::generate();
471 let receiver_keys = Keys::generate();
472
473 let wrapped = wrap_message(
474 &sample_order_message(Some(1)),
475 &trade_keys,
476 &trade_keys,
477 receiver_keys.public_key(),
478 WrapOptions::default(),
479 )
480 .await
481 .expect("wrap");
482
483 let unwrapped = unwrap_message(&wrapped, &receiver_keys)
484 .await
485 .expect("unwrap")
486 .expect("some");
487
488 assert_eq!(unwrapped.sender, trade_keys.public_key());
489 assert_eq!(unwrapped.identity, trade_keys.public_key());
490 }
491
492 #[tokio::test]
493 async fn unwrap_with_corrupted_seal_returns_err() {
494 let receiver_keys = Keys::generate();
495 let ephemeral = Keys::generate();
496
497 let encrypted = nip44::encrypt(
501 ephemeral.secret_key(),
502 &receiver_keys.public_key(),
503 "not a seal",
504 nip44::Version::default(),
505 )
506 .expect("encrypt");
507
508 let corrupted = EventBuilder::new(Kind::GiftWrap, encrypted)
509 .tags([Tag::public_key(receiver_keys.public_key())])
510 .finalize(&ephemeral)
511 .expect("sign");
512
513 let result = unwrap_message(&corrupted, &receiver_keys).await;
514 assert!(
515 matches!(result, Err(MostroError::MostroInternalErr(_))),
516 "expected Err for corrupted gift wrap, got {result:?}",
517 );
518 }
519
520 async fn wrap_with_raw_inner(
524 identity_keys: &Keys,
525 trade_keys: &Keys,
526 receiver: PublicKey,
527 inner: (&Message, Option<String>),
528 ) -> Event {
529 let content = serde_json::to_string(&inner).unwrap();
530 let mut rumor =
531 EventBuilder::new(Kind::TextNote, content).finalize_unsigned(trade_keys.public_key());
532 rumor.ensure_id();
535 let seal = GiftWrapSealBuilder::new(rumor, receiver)
536 .finalize(identity_keys)
537 .unwrap();
538 gift_wrap_from_seal_with_pow(&seal, receiver, 0, None).unwrap()
539 }
540
541 #[tokio::test]
542 async fn unwrap_with_malformed_signature_errors() {
543 let identity_keys = Keys::generate();
544 let trade_keys = Keys::generate();
545 let receiver_keys = Keys::generate();
546 let msg = sample_order_message(Some(1));
547
548 let wrapped = wrap_with_raw_inner(
549 &identity_keys,
550 &trade_keys,
551 receiver_keys.public_key(),
552 (&msg, Some("not-a-hex-signature".to_string())),
553 )
554 .await;
555
556 let result = unwrap_message(&wrapped, &receiver_keys).await;
557 assert!(
558 matches!(result, Err(MostroError::MostroInternalErr(_))),
559 "malformed signature must surface as Err, got {result:?}",
560 );
561 }
562
563 #[tokio::test]
564 async fn unwrap_with_signature_for_other_content_errors() {
565 let identity_keys = Keys::generate();
566 let trade_keys = Keys::generate();
567 let receiver_keys = Keys::generate();
568 let msg = sample_order_message(Some(1));
569 let bogus = Message::sign("not the real message".to_string(), &trade_keys);
571
572 let wrapped = wrap_with_raw_inner(
573 &identity_keys,
574 &trade_keys,
575 receiver_keys.public_key(),
576 (&msg, Some(bogus.to_string())),
577 )
578 .await;
579
580 let result = unwrap_message(&wrapped, &receiver_keys).await;
581 assert!(
582 matches!(result, Err(MostroError::MostroInternalErr(_))),
583 "non-verifying signature must surface as Err, got {result:?}",
584 );
585 }
586
587 #[tokio::test]
588 async fn unwrap_with_wrong_receiver_keys_returns_none() {
589 let identity_keys = Keys::generate();
590 let trade_keys = Keys::generate();
591 let receiver_keys = Keys::generate();
592 let stranger_keys = Keys::generate();
593
594 let wrapped = wrap_message(
595 &sample_order_message(Some(1)),
596 &identity_keys,
597 &trade_keys,
598 receiver_keys.public_key(),
599 WrapOptions::default(),
600 )
601 .await
602 .expect("wrap");
603
604 let result = unwrap_message(&wrapped, &stranger_keys)
605 .await
606 .expect("call should not error");
607 assert!(result.is_none());
608 }
609
610 #[tokio::test]
611 async fn signature_is_verifiable_with_trade_pubkey() {
612 let identity_keys = Keys::generate();
613 let trade_keys = Keys::generate();
614 let receiver_keys = Keys::generate();
615 let message = sample_order_message(Some(7));
616
617 let wrapped = wrap_message(
618 &message,
619 &identity_keys,
620 &trade_keys,
621 receiver_keys.public_key(),
622 WrapOptions::default(),
623 )
624 .await
625 .unwrap();
626
627 let unwrapped = unwrap_message(&wrapped, &receiver_keys)
628 .await
629 .unwrap()
630 .unwrap();
631
632 let sig = unwrapped.signature.expect("signed");
633 let json = unwrapped.message.as_json().unwrap();
634 assert!(Message::verify_signature(
635 json,
636 trade_keys.public_key(),
637 sig
638 ));
639 }
640
641 #[tokio::test]
642 async fn unsigned_wrap_has_no_signature() {
643 let identity_keys = Keys::generate();
644 let trade_keys = Keys::generate();
645 let receiver_keys = Keys::generate();
646
647 let wrapped = wrap_message(
648 &sample_order_message(Some(3)),
649 &identity_keys,
650 &trade_keys,
651 receiver_keys.public_key(),
652 WrapOptions {
653 signed: false,
654 ..WrapOptions::default()
655 },
656 )
657 .await
658 .expect("wrap");
659
660 let unwrapped = unwrap_message(&wrapped, &receiver_keys)
661 .await
662 .unwrap()
663 .unwrap();
664 assert!(unwrapped.signature.is_none());
665 }
666
667 #[tokio::test]
668 async fn expiration_tag_is_set_when_provided() {
669 let identity_keys = Keys::generate();
670 let trade_keys = Keys::generate();
671 let receiver_keys = Keys::generate();
672 let exp = Timestamp::from_secs(Timestamp::now().as_secs() + 3600);
673
674 let wrapped = wrap_message(
675 &sample_order_message(Some(1)),
676 &identity_keys,
677 &trade_keys,
678 receiver_keys.public_key(),
679 WrapOptions {
680 expiration: Some(exp),
681 ..WrapOptions::default()
682 },
683 )
684 .await
685 .expect("wrap");
686
687 let has_expiration = wrapped
688 .tags
689 .iter()
690 .any(|t| t.as_slice().first().map(|s| s.as_str()) == Some("expiration"));
691 assert!(has_expiration);
692 }
693
694 #[test]
695 fn validate_response_cant_do_short_circuits() {
696 let msg = Message::cant_do(
697 Some(uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23")),
698 Some(5),
699 Some(Payload::CantDo(Some(CantDoReason::NotAuthorized))),
700 );
701 let err = validate_response(&msg, Some(5)).unwrap_err();
702 match err {
703 MostroError::MostroCantDo(CantDoReason::NotAuthorized) => {}
704 _ => panic!("expected CantDo(NotAuthorized)"),
705 }
706 }
707
708 #[test]
709 fn validate_response_request_id_match() {
710 let msg = sample_order_message(Some(9));
711 validate_response(&msg, Some(9)).unwrap();
712 }
713
714 #[test]
715 fn validate_response_request_id_mismatch_errors() {
716 let msg = sample_order_message(Some(9));
717 let err = validate_response(&msg, Some(10)).unwrap_err();
718 assert!(matches!(err, MostroError::MostroInternalErr(_)));
719 }
720
721 #[test]
722 fn validate_response_allows_unsolicited_actions_without_request_id() {
723 let msg = Message::Order(MessageKind::new(
724 Some(uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23")),
725 None,
726 None,
727 Action::BuyerTookOrder,
728 None,
729 ));
730 validate_response(&msg, Some(1)).unwrap();
731 }
732
733 #[test]
734 fn validate_response_with_no_expected_id_is_ok() {
735 let msg = sample_order_message(None);
736 validate_response(&msg, None).unwrap();
737 }
738
739 #[test]
740 fn validate_response_allows_cashu_server_events_without_request_id() {
741 for action in [Action::CashuEscrowLocked, Action::CashuPmSignature] {
744 let msg = Message::Order(MessageKind::new(
745 Some(uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23")),
746 None,
747 None,
748 action,
749 None,
750 ));
751 validate_response(&msg, Some(1)).unwrap();
752 }
753 }
754}