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 if wrap_kind != KIND_WRAP && wrap_kind != KIND_WRAP_EPHEMERAL {
275 return Err(StreamError::BadWrapKind(wrap_kind));
276 }
277 let seal_json = seal.as_json();
278 cap(seal_json.len())?;
279 let ct = encrypt_to_bytes(group.conv_key(), seal_json.as_bytes()).map_err(|e| StreamError::Encrypt(e.to_string()))?;
280 let ephemeral = Keys::generate();
281 let wrap = EventBuilder::new(Kind::Custom(wrap_kind), base64_simd::STANDARD.encode_to_string(&ct))
282 .tags([Tag::public_key(ephemeral.public_key())])
283 .custom_created_at(wrap_at)
284 .sign_with_keys(group.keys())
285 .map_err(|e| StreamError::Sign(e.to_string()))?;
286 Ok((wrap, ephemeral))
287}
288
289pub fn rewrap_seal(seal: &Event, new_group: &GroupKey, wrap_at: Timestamp) -> Result<(Event, Keys), StreamError> {
294 if seal.kind.as_u16() != KIND_SEAL_PLAINTEXT {
295 return Err(StreamError::NotRewrappable);
296 }
297 wrap_seal(seal, new_group, KIND_WRAP, wrap_at)
298}
299
300pub fn open_wrap(wrap: &Event, group: &GroupKey) -> Result<OpenedStream, StreamError> {
311 let wrap_kind = wrap.kind.as_u16();
312 if wrap_kind != KIND_WRAP && wrap_kind != KIND_WRAP_EPHEMERAL {
313 return Err(StreamError::BadWrapKind(wrap_kind));
314 }
315 if wrap.pubkey != group.pk() {
316 return Err(StreamError::WrongStream);
317 }
318
319 let seal_json = open_nip44(group.conv_key(), &wrap.content)?;
320 let seal: Event = Event::from_json(&seal_json).map_err(|e| StreamError::Parse(e.to_string()))?;
321 let seal_form = SealForm::from_kind(seal.kind.as_u16()).ok_or(StreamError::BadSealKind(seal.kind.as_u16()))?;
322 seal.verify().map_err(|_| StreamError::BadSealSignature)?;
323
324 let rumor_json = match seal_form {
325 SealForm::Plaintext => seal.content.clone(),
326 SealForm::Encrypted => open_nip44(group.conv_key(), &seal.content)?,
327 };
328 let mut rumor: UnsignedEvent = UnsignedEvent::from_json(rumor_json.as_bytes()).map_err(|e| StreamError::Parse(e.to_string()))?;
329
330 if rumor.pubkey != seal.pubkey {
331 return Err(StreamError::AuthorMismatch);
332 }
333 let computed = EventId::new(&rumor.pubkey, &rumor.created_at, &rumor.kind, &rumor.tags, &rumor.content);
337 if let Some(claimed) = rumor.id {
338 if claimed != computed {
339 return Err(StreamError::BadRumorId);
340 }
341 }
342 rumor.id = Some(computed);
343 let at_ms = resolve_ms_strict(&rumor)?;
344
345 Ok(OpenedStream {
346 rumor_id: computed,
347 author: seal.pubkey,
348 seal_form,
349 seal,
350 wrapper_id: wrap.id,
351 at_ms,
352 rumor,
353 })
354}
355
356pub fn check_channel_binding(rumor: &UnsignedEvent, channel_id: &ChannelId, epoch: Epoch) -> Result<(), StreamError> {
360 match unique_tag_unsigned(rumor, TAG_CHANNEL)? {
361 Some(c) if c == channel_id.to_hex() => {}
362 Some(_) => return Err(StreamError::ChannelMismatch),
363 None => return Err(StreamError::MissingTag(TAG_CHANNEL)),
364 }
365 match unique_tag_unsigned(rumor, TAG_EPOCH)? {
366 Some(e) if e == epoch.0.to_string() => {}
367 Some(_) => return Err(StreamError::EpochMismatch),
368 None => return Err(StreamError::MissingTag(TAG_EPOCH)),
369 }
370 Ok(())
371}
372
373pub fn channel_binding_tags(channel_id: &ChannelId, epoch: Epoch) -> Vec<Tag> {
375 vec![
376 Tag::custom(TagKind::Custom(TAG_CHANNEL.into()), [channel_id.to_hex()]),
377 Tag::custom(TagKind::Custom(TAG_EPOCH.into()), [epoch.0.to_string()]),
378 ]
379}
380
381fn cap(len: usize) -> Result<(), StreamError> {
384 if len > NIP44_MAX_PLAINTEXT {
385 return Err(StreamError::Oversize(len));
386 }
387 Ok(())
388}
389
390fn open_nip44(conv_key: &ConversationKey, content_b64: &str) -> Result<String, StreamError> {
391 let ct = base64_simd::STANDARD
392 .decode_to_vec(content_b64.as_bytes())
393 .map_err(|e| StreamError::Decrypt(e.to_string()))?;
394 let pt = decrypt_to_bytes(conv_key, &ct).map_err(|e| StreamError::Decrypt(e.to_string()))?;
395 String::from_utf8(pt).map_err(|e| StreamError::Parse(e.to_string()))
396}
397
398fn unique_tag_unsigned(rumor: &UnsignedEvent, name: &'static str) -> Result<Option<String>, StreamError> {
402 let mut found: Option<String> = None;
403 for t in rumor.tags.iter() {
404 let s = t.as_slice();
405 if s.len() >= 2 && s[0] == name {
406 if found.is_some() {
407 return Err(StreamError::DuplicateTag(name));
408 }
409 found = Some(s[1].clone());
410 }
411 }
412 Ok(found)
413}
414
415#[cfg(test)]
416mod tests {
417 use super::super::super::{ChannelId, Epoch};
418 use super::super::derive::channel_group_key;
419 use super::super::kind;
420 use super::*;
421
422 fn group() -> GroupKey {
423 channel_group_key(&[7u8; 32], &chan(), Epoch(0))
424 }
425
426 fn chan() -> ChannelId {
427 ChannelId([0xabu8; 32])
428 }
429
430 fn send(author: &Keys, group: &GroupKey, form: SealForm, content: &str, at_ms: u64) -> Event {
431 let tags = channel_binding_tags(&chan(), Epoch(0));
432 let rumor = build_rumor_ms(kind::MESSAGE, author.public_key(), content, tags, at_ms);
433 let seal = build_seal(&rumor, form, group, author).unwrap();
434 wrap_seal(&seal, group, KIND_WRAP, Timestamp::from_secs(1_700_000_000)).unwrap().0
435 }
436
437 #[test]
438 fn encrypted_round_trip_preserves_author_content_and_ms() {
439 let author = Keys::generate();
440 let wrap = send(&author, &group(), SealForm::Encrypted, "Hey chat!", 1_686_840_217_417);
441 assert_eq!(wrap.kind.as_u16(), KIND_WRAP);
442 assert_eq!(wrap.pubkey, group().pk(), "wrap is signed by the stream key");
443
444 let opened = open_wrap(&wrap, &group()).unwrap();
445 assert_eq!(opened.author, author.public_key());
446 assert_eq!(opened.rumor.content, "Hey chat!");
447 assert_eq!(opened.at_ms, 1_686_840_217_417);
448 assert_eq!(opened.seal_form, SealForm::Encrypted);
449 check_channel_binding(&opened.rumor, &chan(), Epoch(0)).unwrap();
450 }
451
452 #[test]
453 fn plaintext_seal_round_trip_carries_rumor_verbatim() {
454 let author = Keys::generate();
455 let wrap = send(&author, &group(), SealForm::Plaintext, "an edition", 1_686_840_217_000);
456 let opened = open_wrap(&wrap, &group()).unwrap();
457 assert_eq!(opened.seal_form, SealForm::Plaintext);
458 assert_eq!(opened.seal.content, opened.rumor.as_json());
460 }
461
462 #[test]
463 fn wrong_stream_key_cannot_open() {
464 let author = Keys::generate();
465 let wrap = send(&author, &group(), SealForm::Encrypted, "secret", 1_000);
466 let other = channel_group_key(&[8u8; 32], &chan(), Epoch(0));
467 assert!(matches!(open_wrap(&wrap, &other), Err(StreamError::WrongStream)));
469 }
470
471 #[test]
472 fn tampered_wrap_content_fails_the_mac() {
473 let author = Keys::generate();
474 let mut wrap = send(&author, &group(), SealForm::Encrypted, "x", 1_000);
475 let mut json: serde_json::Value = serde_json::from_str(&wrap.as_json()).unwrap();
476 let ct = json["content"].as_str().unwrap().to_string();
477 let mut bytes = ct.into_bytes();
480 bytes[20] = if bytes[20] == b'B' { b'C' } else { b'B' };
481 json["content"] = serde_json::Value::String(String::from_utf8(bytes).unwrap());
482 wrap = Event::from_json(json.to_string()).unwrap();
483 assert!(matches!(open_wrap(&wrap, &group()), Err(StreamError::Decrypt(_))));
484 }
485
486 #[test]
487 fn forged_seal_signature_is_rejected() {
488 let author = Keys::generate();
489 let impostor = Keys::generate();
490 let tags = channel_binding_tags(&chan(), Epoch(0));
491 let rumor = build_rumor_ms(kind::MESSAGE, author.public_key(), "hi", tags, 1_000);
492 let seal = build_seal(&rumor, SealForm::Encrypted, &group(), &impostor).unwrap();
495 let mut json: serde_json::Value = serde_json::from_str(&seal.as_json()).unwrap();
496 json["pubkey"] = serde_json::Value::String(author.public_key().to_hex());
497 let forged = Event::from_json(json.to_string());
500 let Ok(forged) = forged else { return }; let (wrap, _) = wrap_seal(&forged, &group(), KIND_WRAP, Timestamp::from_secs(1)).unwrap();
502 assert!(matches!(open_wrap(&wrap, &group()), Err(StreamError::BadSealSignature)));
503 }
504
505 #[test]
506 fn rumor_author_must_match_seal_author() {
507 let author = Keys::generate();
508 let other = Keys::generate();
509 let tags = channel_binding_tags(&chan(), Epoch(0));
510 let rumor = build_rumor_ms(kind::MESSAGE, other.public_key(), "spoof", tags, 1_000);
512 let seal = build_seal(&rumor, SealForm::Encrypted, &group(), &author).unwrap();
513 let (wrap, _) = wrap_seal(&seal, &group(), KIND_WRAP, Timestamp::from_secs(1)).unwrap();
514 assert!(matches!(open_wrap(&wrap, &group()), Err(StreamError::AuthorMismatch)));
515 }
516
517 #[test]
518 fn forged_rumor_id_is_rejected() {
519 let author = Keys::generate();
520 let tags = channel_binding_tags(&chan(), Epoch(0));
521 let rumor = build_rumor_ms(kind::MESSAGE, author.public_key(), "real", tags, 1_000);
522 let mut json: serde_json::Value = serde_json::from_str(&rumor.as_json()).unwrap();
523 json["id"] = serde_json::Value::String("00".repeat(32));
524 let forged_json = json.to_string();
525 let seal = EventBuilder::new(Kind::Custom(KIND_SEAL_PLAINTEXT), forged_json)
528 .custom_created_at(rumor.created_at)
529 .sign_with_keys(&author)
530 .unwrap();
531 let (wrap, _) = wrap_seal(&seal, &group(), KIND_WRAP, Timestamp::from_secs(1)).unwrap();
532 assert!(matches!(open_wrap(&wrap, &group()), Err(StreamError::BadRumorId)));
533 }
534
535 #[test]
536 fn ms_is_strict_absent_is_zero_invalid_is_dropped() {
537 let author = Keys::generate();
538 let rumor = build_rumor_secs(kind::MESSAGE, author.public_key(), "x", vec![], 1_000);
540 assert_eq!(resolve_ms_strict(&rumor).unwrap(), 1_000_000);
541 let ok = build_rumor_secs(
543 kind::MESSAGE,
544 author.public_key(),
545 "x",
546 vec![Tag::custom(TagKind::Custom("ms".into()), ["999".to_string()])],
547 1_000,
548 );
549 assert_eq!(resolve_ms_strict(&ok).unwrap(), 1_000_999);
550 for bad in ["1000", "-1", "12.5", "abc", "007", "", "+5", "+0", "+000", "+999"] {
554 let r = build_rumor_secs(
555 kind::MESSAGE,
556 author.public_key(),
557 "x",
558 vec![Tag::custom(TagKind::Custom("ms".into()), [bad.to_string()])],
559 1_000,
560 );
561 assert!(
562 matches!(resolve_ms_strict(&r), Err(StreamError::BadMs)),
563 "ms={bad:?} must be malformed"
564 );
565 }
566 }
567
568 #[test]
569 fn a_valueless_or_duplicated_ms_tag_is_malformed_not_silently_zero() {
570 let author = Keys::generate();
573 let bare = build_rumor_secs(
574 kind::MESSAGE,
575 author.public_key(),
576 "x",
577 vec![Tag::custom(TagKind::Custom("ms".into()), Vec::<String>::new())],
578 1_000,
579 );
580 assert!(matches!(resolve_ms_strict(&bare), Err(StreamError::BadMs)));
581 let two = build_rumor_secs(
584 kind::MESSAGE,
585 author.public_key(),
586 "x",
587 vec![
588 Tag::custom(TagKind::Custom("ms".into()), Vec::<String>::new()),
589 Tag::custom(TagKind::Custom("ms".into()), ["5".to_string()]),
590 ],
591 1_000,
592 );
593 assert!(matches!(resolve_ms_strict(&two), Err(StreamError::BadMs)));
594 let two_valued = build_rumor_secs(
596 kind::MESSAGE,
597 author.public_key(),
598 "x",
599 vec![
600 Tag::custom(TagKind::Custom("ms".into()), ["1".to_string()]),
601 Tag::custom(TagKind::Custom("ms".into()), ["2".to_string()]),
602 ],
603 1_000,
604 );
605 assert!(matches!(resolve_ms_strict(&two_valued), Err(StreamError::BadMs)));
606 }
607
608 #[test]
609 fn binding_rejects_splices_and_duplicates() {
610 let author = Keys::generate();
611 let tags = channel_binding_tags(&chan(), Epoch(0));
612 let rumor = build_rumor_ms(kind::MESSAGE, author.public_key(), "x", tags, 1_000);
613 assert!(matches!(
615 check_channel_binding(&rumor, &ChannelId([0xcd; 32]), Epoch(0)),
616 Err(StreamError::ChannelMismatch)
617 ));
618 assert!(matches!(
619 check_channel_binding(&rumor, &chan(), Epoch(1)),
620 Err(StreamError::EpochMismatch)
621 ));
622 let mut tags = channel_binding_tags(&chan(), Epoch(0));
624 tags.extend(channel_binding_tags(&chan(), Epoch(0)));
625 let dup = build_rumor_ms(kind::MESSAGE, author.public_key(), "x", tags, 1_000);
626 assert!(matches!(
627 check_channel_binding(&dup, &chan(), Epoch(0)),
628 Err(StreamError::DuplicateTag(_))
629 ));
630 let bare = build_rumor_ms(kind::MESSAGE, author.public_key(), "x", vec![], 1_000);
632 assert!(matches!(
633 check_channel_binding(&bare, &chan(), Epoch(0)),
634 Err(StreamError::MissingTag(_))
635 ));
636 }
637
638 #[test]
639 fn oversize_plaintext_is_refused_at_build_time() {
640 let author = Keys::generate();
641 let big = "x".repeat(NIP44_MAX_PLAINTEXT + 1);
642 let rumor = build_rumor_ms(kind::MESSAGE, author.public_key(), &big, vec![], 1_000);
643 assert!(matches!(
644 seal_content(&rumor, SealForm::Encrypted, &group()),
645 Err(StreamError::Oversize(_))
646 ));
647 }
648
649 #[test]
650 fn ephemeral_wrap_round_trips_and_bad_wrap_kind_rejects() {
651 let author = Keys::generate();
652 let tags = channel_binding_tags(&chan(), Epoch(0));
653 let rumor = build_rumor_ms(kind::TYPING, author.public_key(), "", tags, 5_000);
654 let seal = build_seal(&rumor, SealForm::Encrypted, &group(), &author).unwrap();
655 let (wrap, _) = wrap_seal(&seal, &group(), KIND_WRAP_EPHEMERAL, Timestamp::from_secs(5)).unwrap();
656 assert_eq!(wrap.kind.as_u16(), KIND_WRAP_EPHEMERAL);
657 assert_eq!(open_wrap(&wrap, &group()).unwrap().rumor.kind.as_u16(), kind::TYPING);
658 assert!(matches!(
659 wrap_seal(&seal, &group(), 1058, Timestamp::from_secs(5)),
660 Err(StreamError::BadWrapKind(1058))
661 ));
662 }
663
664 #[test]
665 fn rewrap_preserves_rumor_id_and_signature_across_epochs() {
666 let author = Keys::generate();
667 let wrap = send(&author, &group(), SealForm::Plaintext, "the head edition", 9_000);
668 let opened = open_wrap(&wrap, &group()).unwrap();
669
670 let next = channel_group_key(&[7u8; 32], &chan(), Epoch(1));
672 let (rewrapped, _) = rewrap_seal(&opened.seal, &next, Timestamp::from_secs(2_000)).unwrap();
673 let reopened = open_wrap(&rewrapped, &next).unwrap();
674
675 assert_eq!(reopened.rumor_id, opened.rumor_id, "rumor id survives the re-wrap");
676 assert_eq!(reopened.author, author.public_key(), "authorship survives");
677 assert_eq!(reopened.seal.sig, opened.seal.sig, "the original signature rides verbatim");
678 assert_ne!(reopened.wrapper_id, opened.wrapper_id, "outer identity differs per wrap");
679
680 let enc = send(&author, &group(), SealForm::Encrypted, "no", 9_000);
682 let enc_opened = open_wrap(&enc, &group()).unwrap();
683 assert!(matches!(
684 rewrap_seal(&enc_opened.seal, &next, Timestamp::from_secs(2_000)),
685 Err(StreamError::NotRewrappable)
686 ));
687 }
688
689 #[test]
690 fn wrap_p_tag_is_ephemeral_not_the_stream_or_author() {
691 let author = Keys::generate();
692 let g = group();
693 let tags = channel_binding_tags(&chan(), Epoch(0));
694 let rumor = build_rumor_ms(kind::MESSAGE, author.public_key(), "x", tags, 1_000);
695 let seal = build_seal(&rumor, SealForm::Encrypted, &g, &author).unwrap();
696 let (wrap, ephemeral) = wrap_seal(&seal, &g, KIND_WRAP, Timestamp::from_secs(1)).unwrap();
697 let p = wrap
698 .tags
699 .iter()
700 .find_map(|t| {
701 let s = t.as_slice();
702 (s.len() >= 2 && s[0] == "p").then(|| s[1].clone())
703 })
704 .expect("wrap carries a p tag");
705 assert_eq!(p, ephemeral.public_key().to_hex());
706 assert_ne!(p, g.pk_hex());
707 assert_ne!(p, author.public_key().to_hex());
708 }
709}