1use nostr_sdk::nips::nip44::v2::{decrypt_to_bytes, encrypt_to_bytes, ConversationKey};
26use nostr_sdk::prelude::{Event, EventBuilder, EventId, JsonUtil, Keys, Kind, PublicKey, Tag, TagKind, Timestamp, UnsignedEvent};
27
28use super::super::{ChannelId, Epoch};
29use super::derive::GroupKey;
30
31pub const KIND_WRAP: u16 = 1059;
33pub const KIND_WRAP_EPHEMERAL: u16 = 21059;
35pub const KIND_SEAL_ENCRYPTED: u16 = 20013;
37pub const KIND_SEAL_PLAINTEXT: u16 = 20014;
39
40pub const NIP44_MAX_PLAINTEXT: usize = 65_535;
42
43const TAG_MS: &str = "ms";
44const TAG_CHANNEL: &str = "channel";
45const TAG_EPOCH: &str = "epoch";
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum SealForm {
51 Encrypted,
52 Plaintext,
53}
54
55impl SealForm {
56 pub fn kind(self) -> u16 {
57 match self {
58 SealForm::Encrypted => KIND_SEAL_ENCRYPTED,
59 SealForm::Plaintext => KIND_SEAL_PLAINTEXT,
60 }
61 }
62
63 fn from_kind(kind: u16) -> Option<Self> {
64 match kind {
65 KIND_SEAL_ENCRYPTED => Some(SealForm::Encrypted),
66 KIND_SEAL_PLAINTEXT => Some(SealForm::Plaintext),
67 _ => None,
68 }
69 }
70}
71
72#[derive(Debug)]
74pub enum StreamError {
75 Sign(String),
76 Encrypt(String),
77 Decrypt(String),
78 Parse(String),
79 Oversize(usize),
81 BadWrapKind(u16),
83 WrongStream,
85 BadSealKind(u16),
87 BadSealSignature,
89 AuthorMismatch,
91 BadRumorId,
93 BadMs,
96 ChannelMismatch,
98 EpochMismatch,
100 MissingTag(&'static str),
101 DuplicateTag(&'static str),
103 NotRewrappable,
106}
107
108impl std::fmt::Display for StreamError {
109 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110 match self {
111 StreamError::Sign(e) => write!(f, "sign: {e}"),
112 StreamError::Encrypt(e) => write!(f, "encrypt: {e}"),
113 StreamError::Decrypt(e) => write!(f, "decrypt: {e}"),
114 StreamError::Parse(e) => write!(f, "parse: {e}"),
115 StreamError::Oversize(n) => write!(f, "plaintext {n} bytes exceeds NIP-44 cap"),
116 StreamError::BadWrapKind(k) => write!(f, "not a stream wrap kind: {k}"),
117 StreamError::WrongStream => write!(f, "wrap author is not this stream"),
118 StreamError::BadSealKind(k) => write!(f, "not a seal kind: {k}"),
119 StreamError::BadSealSignature => write!(f, "seal signature invalid"),
120 StreamError::AuthorMismatch => write!(f, "rumor pubkey != seal pubkey"),
121 StreamError::BadRumorId => write!(f, "rumor id != computed hash"),
122 StreamError::BadMs => write!(f, "ms tag outside 0..=999"),
123 StreamError::ChannelMismatch => write!(f, "channel-binding mismatch (splice)"),
124 StreamError::EpochMismatch => write!(f, "epoch-binding mismatch (splice/replay)"),
125 StreamError::MissingTag(t) => write!(f, "missing rumor tag: {t}"),
126 StreamError::DuplicateTag(t) => write!(f, "duplicate rumor tag: {t}"),
127 StreamError::NotRewrappable => write!(f, "only plaintext seals survive re-wrapping"),
128 }
129 }
130}
131
132impl std::error::Error for StreamError {}
133
134#[derive(Debug, Clone)]
136pub struct OpenedStream {
137 pub rumor: UnsignedEvent,
139 pub rumor_id: EventId,
141 pub author: PublicKey,
143 pub seal_form: SealForm,
145 pub seal: Event,
148 pub wrapper_id: EventId,
150 pub at_ms: u64,
152}
153
154pub fn split_ms(at_ms: u64) -> (u64, u16) {
158 (at_ms / 1000, (at_ms % 1000) as u16)
159}
160
161pub fn resolve_ms_strict(rumor: &UnsignedEvent) -> Result<u64, StreamError> {
172 let secs = rumor.created_at.as_secs();
173 let mut offset: Option<u64> = None;
174 for t in rumor.tags.iter() {
175 let s = t.as_slice();
176 if s.first().map(|k| k.as_str()) != Some(TAG_MS) {
177 continue;
178 }
179 if offset.is_some() {
180 return Err(StreamError::BadMs);
182 }
183 let raw = s.get(1).ok_or(StreamError::BadMs)?;
188 if raw.is_empty() || !raw.bytes().all(|b| b.is_ascii_digit()) {
189 return Err(StreamError::BadMs);
190 }
191 let n: u64 = raw.parse().map_err(|_| StreamError::BadMs)?;
192 if n > 999 || (raw.len() > 1 && raw.starts_with('0')) {
193 return Err(StreamError::BadMs);
194 }
195 offset = Some(n);
196 }
197 Ok(secs.saturating_mul(1000).saturating_add(offset.unwrap_or(0)))
198}
199
200pub fn build_rumor_ms(
205 kind: u16,
206 author: PublicKey,
207 content: &str,
208 mut tags: Vec<Tag>,
209 at_ms: u64,
210) -> UnsignedEvent {
211 let (secs, offset) = split_ms(at_ms);
212 tags.push(Tag::custom(TagKind::Custom(TAG_MS.into()), [offset.to_string()]));
213 build_rumor_secs(kind, author, content, tags, secs)
214}
215
216pub fn build_rumor_secs(
219 kind: u16,
220 author: PublicKey,
221 content: &str,
222 tags: Vec<Tag>,
223 at_secs: u64,
224) -> UnsignedEvent {
225 let mut rumor = EventBuilder::new(Kind::Custom(kind), content)
229 .tags(tags)
230 .allow_self_tagging()
231 .custom_created_at(Timestamp::from_secs(at_secs))
232 .build(author);
233 rumor.ensure_id();
234 rumor
235}
236
237pub fn seal_content(rumor: &UnsignedEvent, form: SealForm, group: &GroupKey) -> Result<String, StreamError> {
243 let json = rumor.as_json();
244 cap(json.len())?;
245 match form {
246 SealForm::Plaintext => Ok(json),
247 SealForm::Encrypted => {
248 let ct = encrypt_to_bytes(group.conv_key(), json.as_bytes()).map_err(|e| StreamError::Encrypt(e.to_string()))?;
249 Ok(base64_simd::STANDARD.encode_to_string(&ct))
250 }
251 }
252}
253
254pub fn build_seal(rumor: &UnsignedEvent, form: SealForm, group: &GroupKey, author_keys: &Keys) -> Result<Event, StreamError> {
258 let content = seal_content(rumor, form, group)?;
259 EventBuilder::new(Kind::Custom(form.kind()), content)
260 .custom_created_at(rumor.created_at)
261 .sign_with_keys(author_keys)
262 .map_err(|e| StreamError::Sign(e.to_string()))
263}
264
265pub fn wrap_seal(seal: &Event, group: &GroupKey, wrap_kind: u16, wrap_at: Timestamp) -> Result<(Event, Keys), StreamError> {
274 wrap_seal_with_tags(seal, group, wrap_kind, wrap_at, &[])
275}
276
277pub fn wrap_seal_with_tags(
282 seal: &Event,
283 group: &GroupKey,
284 wrap_kind: u16,
285 wrap_at: Timestamp,
286 extra_tags: &[Tag],
287) -> Result<(Event, Keys), StreamError> {
288 if wrap_kind != KIND_WRAP && wrap_kind != KIND_WRAP_EPHEMERAL {
289 return Err(StreamError::BadWrapKind(wrap_kind));
290 }
291 let seal_json = seal.as_json();
292 cap(seal_json.len())?;
293 let ct = encrypt_to_bytes(group.conv_key(), seal_json.as_bytes()).map_err(|e| StreamError::Encrypt(e.to_string()))?;
294 let ephemeral = Keys::generate();
295 let mut tags = vec![Tag::public_key(ephemeral.public_key())];
296 tags.extend_from_slice(extra_tags);
297 let wrap = EventBuilder::new(Kind::Custom(wrap_kind), base64_simd::STANDARD.encode_to_string(&ct))
298 .tags(tags)
299 .custom_created_at(wrap_at)
300 .sign_with_keys(group.keys())
301 .map_err(|e| StreamError::Sign(e.to_string()))?;
302 Ok((wrap, ephemeral))
303}
304
305pub fn rewrap_seal(seal: &Event, new_group: &GroupKey, wrap_at: Timestamp) -> Result<(Event, Keys), StreamError> {
310 if seal.kind.as_u16() != KIND_SEAL_PLAINTEXT {
311 return Err(StreamError::NotRewrappable);
312 }
313 wrap_seal(seal, new_group, KIND_WRAP, wrap_at)
314}
315
316pub fn open_wrap(wrap: &Event, group: &GroupKey) -> Result<OpenedStream, StreamError> {
327 let wrap_kind = wrap.kind.as_u16();
328 if wrap_kind != KIND_WRAP && wrap_kind != KIND_WRAP_EPHEMERAL {
329 return Err(StreamError::BadWrapKind(wrap_kind));
330 }
331 if wrap.pubkey != group.pk() {
332 return Err(StreamError::WrongStream);
333 }
334
335 let seal_json = open_nip44(group.conv_key(), &wrap.content)?;
336 let seal: Event = Event::from_json(&seal_json).map_err(|e| StreamError::Parse(e.to_string()))?;
337 let seal_form = SealForm::from_kind(seal.kind.as_u16()).ok_or(StreamError::BadSealKind(seal.kind.as_u16()))?;
338 seal.verify().map_err(|_| StreamError::BadSealSignature)?;
339
340 let rumor_json = match seal_form {
341 SealForm::Plaintext => seal.content.clone(),
342 SealForm::Encrypted => open_nip44(group.conv_key(), &seal.content)?,
343 };
344 let mut rumor: UnsignedEvent = UnsignedEvent::from_json(rumor_json.as_bytes()).map_err(|e| StreamError::Parse(e.to_string()))?;
345
346 if rumor.pubkey != seal.pubkey {
347 return Err(StreamError::AuthorMismatch);
348 }
349 let computed = EventId::new(&rumor.pubkey, &rumor.created_at, &rumor.kind, &rumor.tags, &rumor.content);
353 if let Some(claimed) = rumor.id {
354 if claimed != computed {
355 return Err(StreamError::BadRumorId);
356 }
357 }
358 rumor.id = Some(computed);
359 let at_ms = resolve_ms_strict(&rumor)?;
360
361 Ok(OpenedStream {
362 rumor_id: computed,
363 author: seal.pubkey,
364 seal_form,
365 seal,
366 wrapper_id: wrap.id,
367 at_ms,
368 rumor,
369 })
370}
371
372pub fn check_channel_binding(rumor: &UnsignedEvent, channel_id: &ChannelId, epoch: Epoch) -> Result<(), StreamError> {
376 match unique_tag_unsigned(rumor, TAG_CHANNEL)? {
377 Some(c) if c == channel_id.to_hex() => {}
378 Some(_) => return Err(StreamError::ChannelMismatch),
379 None => return Err(StreamError::MissingTag(TAG_CHANNEL)),
380 }
381 match unique_tag_unsigned(rumor, TAG_EPOCH)? {
382 Some(e) if e == epoch.0.to_string() => {}
383 Some(_) => return Err(StreamError::EpochMismatch),
384 None => return Err(StreamError::MissingTag(TAG_EPOCH)),
385 }
386 Ok(())
387}
388
389pub fn channel_binding_tags(channel_id: &ChannelId, epoch: Epoch) -> Vec<Tag> {
391 vec![
392 Tag::custom(TagKind::Custom(TAG_CHANNEL.into()), [channel_id.to_hex()]),
393 Tag::custom(TagKind::Custom(TAG_EPOCH.into()), [epoch.0.to_string()]),
394 ]
395}
396
397fn cap(len: usize) -> Result<(), StreamError> {
400 if len > NIP44_MAX_PLAINTEXT {
401 return Err(StreamError::Oversize(len));
402 }
403 Ok(())
404}
405
406fn open_nip44(conv_key: &ConversationKey, content_b64: &str) -> Result<String, StreamError> {
407 let ct = base64_simd::STANDARD
408 .decode_to_vec(content_b64.as_bytes())
409 .map_err(|e| StreamError::Decrypt(e.to_string()))?;
410 let pt = decrypt_to_bytes(conv_key, &ct).map_err(|e| StreamError::Decrypt(e.to_string()))?;
411 String::from_utf8(pt).map_err(|e| StreamError::Parse(e.to_string()))
412}
413
414fn unique_tag_unsigned(rumor: &UnsignedEvent, name: &'static str) -> Result<Option<String>, StreamError> {
418 let mut found: Option<String> = None;
419 for t in rumor.tags.iter() {
420 let s = t.as_slice();
421 if s.len() >= 2 && s[0] == name {
422 if found.is_some() {
423 return Err(StreamError::DuplicateTag(name));
424 }
425 found = Some(s[1].clone());
426 }
427 }
428 Ok(found)
429}
430
431#[cfg(test)]
432mod tests {
433 use super::super::super::{ChannelId, Epoch};
434 use super::super::derive::channel_group_key;
435 use super::super::kind;
436 use super::*;
437
438 fn group() -> GroupKey {
439 channel_group_key(&[7u8; 32], &chan(), Epoch(0))
440 }
441
442 fn chan() -> ChannelId {
443 ChannelId([0xabu8; 32])
444 }
445
446 fn send(author: &Keys, group: &GroupKey, form: SealForm, content: &str, at_ms: u64) -> Event {
447 let tags = channel_binding_tags(&chan(), Epoch(0));
448 let rumor = build_rumor_ms(kind::MESSAGE, author.public_key(), content, tags, at_ms);
449 let seal = build_seal(&rumor, form, group, author).unwrap();
450 wrap_seal(&seal, group, KIND_WRAP, Timestamp::from_secs(1_700_000_000)).unwrap().0
451 }
452
453 #[test]
454 fn encrypted_round_trip_preserves_author_content_and_ms() {
455 let author = Keys::generate();
456 let wrap = send(&author, &group(), SealForm::Encrypted, "Hey chat!", 1_686_840_217_417);
457 assert_eq!(wrap.kind.as_u16(), KIND_WRAP);
458 assert_eq!(wrap.pubkey, group().pk(), "wrap is signed by the stream key");
459
460 let opened = open_wrap(&wrap, &group()).unwrap();
461 assert_eq!(opened.author, author.public_key());
462 assert_eq!(opened.rumor.content, "Hey chat!");
463 assert_eq!(opened.at_ms, 1_686_840_217_417);
464 assert_eq!(opened.seal_form, SealForm::Encrypted);
465 check_channel_binding(&opened.rumor, &chan(), Epoch(0)).unwrap();
466 }
467
468 #[test]
469 fn plaintext_seal_round_trip_carries_rumor_verbatim() {
470 let author = Keys::generate();
471 let wrap = send(&author, &group(), SealForm::Plaintext, "an edition", 1_686_840_217_000);
472 let opened = open_wrap(&wrap, &group()).unwrap();
473 assert_eq!(opened.seal_form, SealForm::Plaintext);
474 assert_eq!(opened.seal.content, opened.rumor.as_json());
476 }
477
478 #[test]
479 fn wrong_stream_key_cannot_open() {
480 let author = Keys::generate();
481 let wrap = send(&author, &group(), SealForm::Encrypted, "secret", 1_000);
482 let other = channel_group_key(&[8u8; 32], &chan(), Epoch(0));
483 assert!(matches!(open_wrap(&wrap, &other), Err(StreamError::WrongStream)));
485 }
486
487 #[test]
488 fn tampered_wrap_content_fails_the_mac() {
489 let author = Keys::generate();
490 let mut wrap = send(&author, &group(), SealForm::Encrypted, "x", 1_000);
491 let mut json: serde_json::Value = serde_json::from_str(&wrap.as_json()).unwrap();
492 let ct = json["content"].as_str().unwrap().to_string();
493 let mut bytes = ct.into_bytes();
496 bytes[20] = if bytes[20] == b'B' { b'C' } else { b'B' };
497 json["content"] = serde_json::Value::String(String::from_utf8(bytes).unwrap());
498 wrap = Event::from_json(json.to_string()).unwrap();
499 assert!(matches!(open_wrap(&wrap, &group()), Err(StreamError::Decrypt(_))));
500 }
501
502 #[test]
503 fn forged_seal_signature_is_rejected() {
504 let author = Keys::generate();
505 let impostor = Keys::generate();
506 let tags = channel_binding_tags(&chan(), Epoch(0));
507 let rumor = build_rumor_ms(kind::MESSAGE, author.public_key(), "hi", tags, 1_000);
508 let seal = build_seal(&rumor, SealForm::Encrypted, &group(), &impostor).unwrap();
511 let mut json: serde_json::Value = serde_json::from_str(&seal.as_json()).unwrap();
512 json["pubkey"] = serde_json::Value::String(author.public_key().to_hex());
513 let forged = Event::from_json(json.to_string());
516 let Ok(forged) = forged else { return }; let (wrap, _) = wrap_seal(&forged, &group(), KIND_WRAP, Timestamp::from_secs(1)).unwrap();
518 assert!(matches!(open_wrap(&wrap, &group()), Err(StreamError::BadSealSignature)));
519 }
520
521 #[test]
522 fn rumor_author_must_match_seal_author() {
523 let author = Keys::generate();
524 let other = Keys::generate();
525 let tags = channel_binding_tags(&chan(), Epoch(0));
526 let rumor = build_rumor_ms(kind::MESSAGE, other.public_key(), "spoof", tags, 1_000);
528 let seal = build_seal(&rumor, SealForm::Encrypted, &group(), &author).unwrap();
529 let (wrap, _) = wrap_seal(&seal, &group(), KIND_WRAP, Timestamp::from_secs(1)).unwrap();
530 assert!(matches!(open_wrap(&wrap, &group()), Err(StreamError::AuthorMismatch)));
531 }
532
533 #[test]
534 fn forged_rumor_id_is_rejected() {
535 let author = Keys::generate();
536 let tags = channel_binding_tags(&chan(), Epoch(0));
537 let rumor = build_rumor_ms(kind::MESSAGE, author.public_key(), "real", tags, 1_000);
538 let mut json: serde_json::Value = serde_json::from_str(&rumor.as_json()).unwrap();
539 json["id"] = serde_json::Value::String("00".repeat(32));
540 let forged_json = json.to_string();
541 let seal = EventBuilder::new(Kind::Custom(KIND_SEAL_PLAINTEXT), forged_json)
544 .custom_created_at(rumor.created_at)
545 .sign_with_keys(&author)
546 .unwrap();
547 let (wrap, _) = wrap_seal(&seal, &group(), KIND_WRAP, Timestamp::from_secs(1)).unwrap();
548 assert!(matches!(open_wrap(&wrap, &group()), Err(StreamError::BadRumorId)));
549 }
550
551 #[test]
552 fn ms_is_strict_absent_is_zero_invalid_is_dropped() {
553 let author = Keys::generate();
554 let rumor = build_rumor_secs(kind::MESSAGE, author.public_key(), "x", vec![], 1_000);
556 assert_eq!(resolve_ms_strict(&rumor).unwrap(), 1_000_000);
557 let ok = build_rumor_secs(
559 kind::MESSAGE,
560 author.public_key(),
561 "x",
562 vec![Tag::custom(TagKind::Custom("ms".into()), ["999".to_string()])],
563 1_000,
564 );
565 assert_eq!(resolve_ms_strict(&ok).unwrap(), 1_000_999);
566 for bad in ["1000", "-1", "12.5", "abc", "007", "", "+5", "+0", "+000", "+999"] {
570 let r = build_rumor_secs(
571 kind::MESSAGE,
572 author.public_key(),
573 "x",
574 vec![Tag::custom(TagKind::Custom("ms".into()), [bad.to_string()])],
575 1_000,
576 );
577 assert!(
578 matches!(resolve_ms_strict(&r), Err(StreamError::BadMs)),
579 "ms={bad:?} must be malformed"
580 );
581 }
582 }
583
584 #[test]
585 fn a_valueless_or_duplicated_ms_tag_is_malformed_not_silently_zero() {
586 let author = Keys::generate();
589 let bare = build_rumor_secs(
590 kind::MESSAGE,
591 author.public_key(),
592 "x",
593 vec![Tag::custom(TagKind::Custom("ms".into()), Vec::<String>::new())],
594 1_000,
595 );
596 assert!(matches!(resolve_ms_strict(&bare), Err(StreamError::BadMs)));
597 let two = build_rumor_secs(
600 kind::MESSAGE,
601 author.public_key(),
602 "x",
603 vec![
604 Tag::custom(TagKind::Custom("ms".into()), Vec::<String>::new()),
605 Tag::custom(TagKind::Custom("ms".into()), ["5".to_string()]),
606 ],
607 1_000,
608 );
609 assert!(matches!(resolve_ms_strict(&two), Err(StreamError::BadMs)));
610 let two_valued = build_rumor_secs(
612 kind::MESSAGE,
613 author.public_key(),
614 "x",
615 vec![
616 Tag::custom(TagKind::Custom("ms".into()), ["1".to_string()]),
617 Tag::custom(TagKind::Custom("ms".into()), ["2".to_string()]),
618 ],
619 1_000,
620 );
621 assert!(matches!(resolve_ms_strict(&two_valued), Err(StreamError::BadMs)));
622 }
623
624 #[test]
625 fn binding_rejects_splices_and_duplicates() {
626 let author = Keys::generate();
627 let tags = channel_binding_tags(&chan(), Epoch(0));
628 let rumor = build_rumor_ms(kind::MESSAGE, author.public_key(), "x", tags, 1_000);
629 assert!(matches!(
631 check_channel_binding(&rumor, &ChannelId([0xcd; 32]), Epoch(0)),
632 Err(StreamError::ChannelMismatch)
633 ));
634 assert!(matches!(
635 check_channel_binding(&rumor, &chan(), Epoch(1)),
636 Err(StreamError::EpochMismatch)
637 ));
638 let mut tags = channel_binding_tags(&chan(), Epoch(0));
640 tags.extend(channel_binding_tags(&chan(), Epoch(0)));
641 let dup = build_rumor_ms(kind::MESSAGE, author.public_key(), "x", tags, 1_000);
642 assert!(matches!(
643 check_channel_binding(&dup, &chan(), Epoch(0)),
644 Err(StreamError::DuplicateTag(_))
645 ));
646 let bare = build_rumor_ms(kind::MESSAGE, author.public_key(), "x", vec![], 1_000);
648 assert!(matches!(
649 check_channel_binding(&bare, &chan(), Epoch(0)),
650 Err(StreamError::MissingTag(_))
651 ));
652 }
653
654 #[test]
655 fn oversize_plaintext_is_refused_at_build_time() {
656 let author = Keys::generate();
657 let big = "x".repeat(NIP44_MAX_PLAINTEXT + 1);
658 let rumor = build_rumor_ms(kind::MESSAGE, author.public_key(), &big, vec![], 1_000);
659 assert!(matches!(
660 seal_content(&rumor, SealForm::Encrypted, &group()),
661 Err(StreamError::Oversize(_))
662 ));
663 }
664
665 #[test]
666 fn ephemeral_wrap_round_trips_and_bad_wrap_kind_rejects() {
667 let author = Keys::generate();
668 let tags = channel_binding_tags(&chan(), Epoch(0));
669 let rumor = build_rumor_ms(kind::TYPING, author.public_key(), "", tags, 5_000);
670 let seal = build_seal(&rumor, SealForm::Encrypted, &group(), &author).unwrap();
671 let (wrap, _) = wrap_seal(&seal, &group(), KIND_WRAP_EPHEMERAL, Timestamp::from_secs(5)).unwrap();
672 assert_eq!(wrap.kind.as_u16(), KIND_WRAP_EPHEMERAL);
673 assert_eq!(open_wrap(&wrap, &group()).unwrap().rumor.kind.as_u16(), kind::TYPING);
674 assert!(matches!(
675 wrap_seal(&seal, &group(), 1058, Timestamp::from_secs(5)),
676 Err(StreamError::BadWrapKind(1058))
677 ));
678 }
679
680 #[test]
681 fn rewrap_preserves_rumor_id_and_signature_across_epochs() {
682 let author = Keys::generate();
683 let wrap = send(&author, &group(), SealForm::Plaintext, "the head edition", 9_000);
684 let opened = open_wrap(&wrap, &group()).unwrap();
685
686 let next = channel_group_key(&[7u8; 32], &chan(), Epoch(1));
688 let (rewrapped, _) = rewrap_seal(&opened.seal, &next, Timestamp::from_secs(2_000)).unwrap();
689 let reopened = open_wrap(&rewrapped, &next).unwrap();
690
691 assert_eq!(reopened.rumor_id, opened.rumor_id, "rumor id survives the re-wrap");
692 assert_eq!(reopened.author, author.public_key(), "authorship survives");
693 assert_eq!(reopened.seal.sig, opened.seal.sig, "the original signature rides verbatim");
694 assert_ne!(reopened.wrapper_id, opened.wrapper_id, "outer identity differs per wrap");
695
696 let enc = send(&author, &group(), SealForm::Encrypted, "no", 9_000);
698 let enc_opened = open_wrap(&enc, &group()).unwrap();
699 assert!(matches!(
700 rewrap_seal(&enc_opened.seal, &next, Timestamp::from_secs(2_000)),
701 Err(StreamError::NotRewrappable)
702 ));
703 }
704
705 #[test]
706 fn wrap_p_tag_is_ephemeral_not_the_stream_or_author() {
707 let author = Keys::generate();
708 let g = group();
709 let tags = channel_binding_tags(&chan(), Epoch(0));
710 let rumor = build_rumor_ms(kind::MESSAGE, author.public_key(), "x", tags, 1_000);
711 let seal = build_seal(&rumor, SealForm::Encrypted, &g, &author).unwrap();
712 let (wrap, ephemeral) = wrap_seal(&seal, &g, KIND_WRAP, Timestamp::from_secs(1)).unwrap();
713 let p = wrap
714 .tags
715 .iter()
716 .find_map(|t| {
717 let s = t.as_slice();
718 (s.len() >= 2 && s[0] == "p").then(|| s[1].clone())
719 })
720 .expect("wrap carries a p tag");
721 assert_eq!(p, ephemeral.public_key().to_hex());
722 assert_ne!(p, g.pk_hex());
723 assert_ne!(p, author.public_key().to_hex());
724 }
725}