1use crate::event_ext::FinalizeUnsignedWithId;
26use nostr_sdk::prelude::FinalizeEvent;
27use nostr_sdk::prelude::nip44::v2::{decrypt_to_bytes, ConversationKey};
28use nostr_sdk::prelude::{Event, EventBuilder, EventId, Keys, Kind, PublicKey, Tag, Timestamp, UnsignedEvent};
29
30use super::super::{ChannelId, Epoch};
31use super::derive::GroupKey;
32
33pub const KIND_WRAP: u16 = 1059;
35pub const KIND_WRAP_EPHEMERAL: u16 = 21059;
37pub const KIND_SEAL_ENCRYPTED: u16 = 20013;
39pub const KIND_SEAL_PLAINTEXT: u16 = 20014;
41
42pub const NIP44_MAX_PLAINTEXT: usize = 65_535;
44
45const TAG_MS: &str = "ms";
46const TAG_CHANNEL: &str = "channel";
47const TAG_EPOCH: &str = "epoch";
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum SealForm {
53 Encrypted,
54 Plaintext,
55}
56
57impl SealForm {
58 pub fn kind(self) -> u16 {
59 match self {
60 SealForm::Encrypted => KIND_SEAL_ENCRYPTED,
61 SealForm::Plaintext => KIND_SEAL_PLAINTEXT,
62 }
63 }
64
65 fn from_kind(kind: u16) -> Option<Self> {
66 match kind {
67 KIND_SEAL_ENCRYPTED => Some(SealForm::Encrypted),
68 KIND_SEAL_PLAINTEXT => Some(SealForm::Plaintext),
69 _ => None,
70 }
71 }
72}
73
74#[derive(Debug)]
76pub enum StreamError {
77 Sign(String),
78 Encrypt(String),
79 Decrypt(String),
80 Parse(String),
81 Oversize(usize),
83 BadWrapKind(u16),
85 WrongStream,
87 BadSealKind(u16),
89 BadSealSignature,
91 AuthorMismatch,
93 BadRumorId,
95 BadMs,
98 ChannelMismatch,
100 EpochMismatch,
102 MissingTag(&'static str),
103 DuplicateTag(&'static str),
105 NotRewrappable,
108}
109
110impl std::fmt::Display for StreamError {
111 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112 match self {
113 StreamError::Sign(e) => write!(f, "sign: {e}"),
114 StreamError::Encrypt(e) => write!(f, "encrypt: {e}"),
115 StreamError::Decrypt(e) => write!(f, "decrypt: {e}"),
116 StreamError::Parse(e) => write!(f, "parse: {e}"),
117 StreamError::Oversize(n) => write!(f, "plaintext {n} bytes exceeds NIP-44 cap"),
118 StreamError::BadWrapKind(k) => write!(f, "not a stream wrap kind: {k}"),
119 StreamError::WrongStream => write!(f, "wrap author is not this stream"),
120 StreamError::BadSealKind(k) => write!(f, "not a seal kind: {k}"),
121 StreamError::BadSealSignature => write!(f, "seal signature invalid"),
122 StreamError::AuthorMismatch => write!(f, "rumor pubkey != seal pubkey"),
123 StreamError::BadRumorId => write!(f, "rumor id != computed hash"),
124 StreamError::BadMs => write!(f, "ms tag outside 0..=999"),
125 StreamError::ChannelMismatch => write!(f, "channel-binding mismatch (splice)"),
126 StreamError::EpochMismatch => write!(f, "epoch-binding mismatch (splice/replay)"),
127 StreamError::MissingTag(t) => write!(f, "missing rumor tag: {t}"),
128 StreamError::DuplicateTag(t) => write!(f, "duplicate rumor tag: {t}"),
129 StreamError::NotRewrappable => write!(f, "only plaintext seals survive re-wrapping"),
130 }
131 }
132}
133
134impl std::error::Error for StreamError {}
135
136#[derive(Debug, Clone)]
138pub struct OpenedStream {
139 pub rumor: UnsignedEvent,
141 pub rumor_id: EventId,
143 pub author: PublicKey,
145 pub seal_form: SealForm,
147 pub seal: Event,
150 pub wrapper_id: EventId,
152 pub at_ms: u64,
154}
155
156pub fn split_ms(at_ms: u64) -> (u64, u16) {
160 (at_ms / 1000, (at_ms % 1000) as u16)
161}
162
163pub fn resolve_ms_strict(rumor: &UnsignedEvent) -> Result<u64, StreamError> {
174 let secs = rumor.created_at.as_secs();
175 let Some(raw) = rumor.tags.iter().find_map(|t| {
184 let s = t.as_slice();
185 (s.first().map(|k| k.as_str()) == Some(TAG_MS)).then(|| s.get(1).cloned())
186 }) else {
187 return Ok(secs.saturating_mul(1000));
188 };
189 let raw = raw.ok_or(StreamError::BadMs)?;
194 if raw.is_empty() || !raw.bytes().all(|b| b.is_ascii_digit()) {
195 return Err(StreamError::BadMs);
196 }
197 let n: u64 = raw.parse().map_err(|_| StreamError::BadMs)?;
198 if n > 999 || (raw.len() > 1 && raw.starts_with('0')) {
199 return Err(StreamError::BadMs);
200 }
201 Ok(secs.saturating_mul(1000).saturating_add(n))
202}
203
204pub fn build_rumor_ms(
209 kind: u16,
210 author: PublicKey,
211 content: &str,
212 mut tags: Vec<Tag>,
213 at_ms: u64,
214) -> UnsignedEvent {
215 let (secs, offset) = split_ms(at_ms);
216 tags.push(Tag::custom(TAG_MS, [offset.to_string()]));
217 build_rumor_secs(kind, author, content, tags, secs)
218}
219
220pub fn build_rumor_secs(
223 kind: u16,
224 author: PublicKey,
225 content: &str,
226 tags: Vec<Tag>,
227 at_secs: u64,
228) -> UnsignedEvent {
229 let mut rumor = EventBuilder::new(Kind::Custom(kind), content)
233 .tags(tags)
234 .custom_created_at(Timestamp::from_secs(at_secs))
235 .finalize_unsigned_with_id(author);
236 rumor.ensure_id();
237 rumor
238}
239
240pub fn seal_content(rumor: &UnsignedEvent, form: SealForm, group: &GroupKey) -> Result<String, StreamError> {
246 let json = rumor.as_json();
247 cap(json.len())?;
248 match form {
249 SealForm::Plaintext => Ok(json),
250 SealForm::Encrypted => {
251 let ct = crate::community::cipher::encrypt_with_random_nonce(group.conv_key(), json.as_bytes()).map_err(|e| StreamError::Encrypt(e.to_string()))?;
252 Ok(base64_simd::STANDARD.encode_to_string(&ct))
253 }
254 }
255}
256
257pub fn build_seal(rumor: &UnsignedEvent, form: SealForm, group: &GroupKey, author_keys: &Keys) -> Result<Event, StreamError> {
261 let content = seal_content(rumor, form, group)?;
262 EventBuilder::new(Kind::Custom(form.kind()), content)
263 .custom_created_at(rumor.created_at)
264 .finalize(author_keys)
265 .map_err(|e| StreamError::Sign(e.to_string()))
266}
267
268pub fn wrap_seal(seal: &Event, group: &GroupKey, wrap_kind: u16, wrap_at: Timestamp) -> Result<(Event, Keys), StreamError> {
277 wrap_seal_with_tags(seal, group, wrap_kind, wrap_at, &[])
278}
279
280pub fn wrap_seal_with_tags(
285 seal: &Event,
286 group: &GroupKey,
287 wrap_kind: u16,
288 wrap_at: Timestamp,
289 extra_tags: &[Tag],
290) -> Result<(Event, Keys), StreamError> {
291 if wrap_kind != KIND_WRAP && wrap_kind != KIND_WRAP_EPHEMERAL {
292 return Err(StreamError::BadWrapKind(wrap_kind));
293 }
294 let seal_json = seal.as_json();
295 cap(seal_json.len())?;
296 let ct = crate::community::cipher::encrypt_with_random_nonce(group.conv_key(), seal_json.as_bytes()).map_err(|e| StreamError::Encrypt(e.to_string()))?;
297 let ephemeral = Keys::generate();
298 let mut tags = vec![Tag::public_key(ephemeral.public_key())];
299 tags.extend_from_slice(extra_tags);
300 let wrap = EventBuilder::new(Kind::Custom(wrap_kind), base64_simd::STANDARD.encode_to_string(&ct))
301 .tags(tags)
302 .custom_created_at(wrap_at)
303 .finalize(group.keys())
304 .map_err(|e| StreamError::Sign(e.to_string()))?;
305 Ok((wrap, ephemeral))
306}
307
308pub async fn seal_and_wrap_signed<S: crate::signer::VectorSigner + ?Sized>(
314 signer: &S,
315 author: PublicKey,
316 rumor: &UnsignedEvent,
317 form: SealForm,
318 group: &GroupKey,
319 wrap_kind: u16,
320 wrap_at: Timestamp,
321 extra_tags: &[Tag],
322) -> Result<(Event, Keys), StreamError> {
323 let content = seal_content(rumor, form, group)?;
324 let unsigned = EventBuilder::new(Kind::Custom(form.kind()), content)
325 .custom_created_at(rumor.created_at)
326 .finalize_unsigned_with_id(author);
327 let seal = signer
328 .sign_event_async(unsigned)
329 .await
330 .map_err(|e| StreamError::Sign(e.to_string()))?;
331 wrap_seal_with_tags(&seal, group, wrap_kind, wrap_at, extra_tags)
332}
333
334pub fn rewrap_seal(seal: &Event, new_group: &GroupKey, wrap_at: Timestamp) -> Result<(Event, Keys), StreamError> {
339 if seal.kind.as_u16() != KIND_SEAL_PLAINTEXT {
340 return Err(StreamError::NotRewrappable);
341 }
342 wrap_seal(seal, new_group, KIND_WRAP, wrap_at)
343}
344
345pub fn open_wrap(wrap: &Event, group: &GroupKey) -> Result<OpenedStream, StreamError> {
356 let wrap_kind = wrap.kind.as_u16();
357 if wrap_kind != KIND_WRAP && wrap_kind != KIND_WRAP_EPHEMERAL {
358 return Err(StreamError::BadWrapKind(wrap_kind));
359 }
360 if wrap.pubkey != group.pk() {
361 return Err(StreamError::WrongStream);
362 }
363
364 let seal_json = open_nip44(group.conv_key(), &wrap.content)?;
365 let seal: Event = Event::from_json(&seal_json).map_err(|e| StreamError::Parse(e.to_string()))?;
366 let seal_form = SealForm::from_kind(seal.kind.as_u16()).ok_or(StreamError::BadSealKind(seal.kind.as_u16()))?;
367 seal.verify().map_err(|_| StreamError::BadSealSignature)?;
368
369 let rumor_json = match seal_form {
370 SealForm::Plaintext => seal.content.clone(),
371 SealForm::Encrypted => open_nip44(group.conv_key(), &seal.content)?,
372 };
373 let mut rumor: UnsignedEvent = UnsignedEvent::from_json(rumor_json.as_bytes()).map_err(|e| StreamError::Parse(e.to_string()))?;
374
375 if rumor.pubkey != seal.pubkey {
376 return Err(StreamError::AuthorMismatch);
377 }
378 let computed = EventId::compute(&rumor.pubkey, &rumor.created_at, &rumor.kind, &rumor.tags, &rumor.content);
382 if let Some(claimed) = rumor.id {
383 if claimed != computed {
384 return Err(StreamError::BadRumorId);
385 }
386 }
387 rumor.id = Some(computed);
388 let at_ms = resolve_ms_strict(&rumor)?;
389
390 Ok(OpenedStream {
391 rumor_id: computed,
392 author: seal.pubkey,
393 seal_form,
394 seal,
395 wrapper_id: wrap.id,
396 at_ms,
397 rumor,
398 })
399}
400
401pub fn check_channel_binding(rumor: &UnsignedEvent, channel_id: &ChannelId, epoch: Epoch) -> Result<(), StreamError> {
405 match unique_tag_unsigned(rumor, TAG_CHANNEL)? {
406 Some(c) if c == channel_id.to_hex() => {}
407 Some(_) => return Err(StreamError::ChannelMismatch),
408 None => return Err(StreamError::MissingTag(TAG_CHANNEL)),
409 }
410 match unique_tag_unsigned(rumor, TAG_EPOCH)? {
411 Some(e) if e == epoch.0.to_string() => {}
412 Some(_) => return Err(StreamError::EpochMismatch),
413 None => return Err(StreamError::MissingTag(TAG_EPOCH)),
414 }
415 Ok(())
416}
417
418pub fn channel_binding_tags(channel_id: &ChannelId, epoch: Epoch) -> Vec<Tag> {
420 vec![
421 Tag::custom(TAG_CHANNEL, [channel_id.to_hex()]),
422 Tag::custom(TAG_EPOCH, [epoch.0.to_string()]),
423 ]
424}
425
426fn cap(len: usize) -> Result<(), StreamError> {
429 if len > NIP44_MAX_PLAINTEXT {
430 return Err(StreamError::Oversize(len));
431 }
432 Ok(())
433}
434
435fn open_nip44(conv_key: &ConversationKey, content_b64: &str) -> Result<String, StreamError> {
436 let ct = base64_simd::STANDARD
437 .decode_to_vec(content_b64.as_bytes())
438 .map_err(|e| StreamError::Decrypt(e.to_string()))?;
439 let pt = decrypt_to_bytes(conv_key, &ct).map_err(|e| StreamError::Decrypt(e.to_string()))?;
440 String::from_utf8(pt).map_err(|e| StreamError::Parse(e.to_string()))
441}
442
443fn unique_tag_unsigned(rumor: &UnsignedEvent, name: &'static str) -> Result<Option<String>, StreamError> {
447 let mut found: Option<String> = None;
448 for t in rumor.tags.iter() {
449 let s = t.as_slice();
450 if s.len() >= 2 && s[0] == name {
451 if found.is_some() {
452 return Err(StreamError::DuplicateTag(name));
453 }
454 found = Some(s[1].clone());
455 }
456 }
457 Ok(found)
458}
459
460#[cfg(test)]
461mod tests {
462
463 fn as_signer(k: &Keys) -> crate::signer::ActiveSigner {
467 crate::signer::ActiveSigner::Keys(k.clone())
468 }
469 use super::super::super::{ChannelId, Epoch};
470 use super::super::derive::channel_group_key;
471 use super::super::kind;
472 use super::*;
473
474 fn group() -> GroupKey {
475 channel_group_key(&[7u8; 32], &chan(), Epoch(0))
476 }
477
478 fn chan() -> ChannelId {
479 ChannelId([0xabu8; 32])
480 }
481
482 fn send(author: &Keys, group: &GroupKey, form: SealForm, content: &str, at_ms: u64) -> Event {
483 let tags = channel_binding_tags(&chan(), Epoch(0));
484 let rumor = build_rumor_ms(kind::MESSAGE, author.public_key(), content, tags, at_ms);
485 let seal = build_seal(&rumor, form, group, author).unwrap();
486 wrap_seal(&seal, group, KIND_WRAP, Timestamp::from_secs(1_700_000_000)).unwrap().0
487 }
488
489 #[test]
490 fn encrypted_round_trip_preserves_author_content_and_ms() {
491 let author = Keys::generate();
492 let wrap = send(&author, &group(), SealForm::Encrypted, "Hey chat!", 1_686_840_217_417);
493 assert_eq!(wrap.kind.as_u16(), KIND_WRAP);
494 assert_eq!(wrap.pubkey, group().pk(), "wrap is signed by the stream key");
495
496 let opened = open_wrap(&wrap, &group()).unwrap();
497 assert_eq!(opened.author, author.public_key());
498 assert_eq!(opened.rumor.content, "Hey chat!");
499 assert_eq!(opened.at_ms, 1_686_840_217_417);
500 assert_eq!(opened.seal_form, SealForm::Encrypted);
501 check_channel_binding(&opened.rumor, &chan(), Epoch(0)).unwrap();
502 }
503
504 #[tokio::test]
505 async fn signer_seal_opens_identically_to_local_seal() {
506 let author = Keys::generate();
510 let g = group();
511 let tags = channel_binding_tags(&chan(), Epoch(0));
512 let rumor = build_rumor_ms(kind::MESSAGE, author.public_key(), "parity", tags, 1_686_840_217_000);
513 let wrap_at = Timestamp::from_secs(1_686_840_217);
514
515 let seal_l = build_seal(&rumor, SealForm::Encrypted, &g, &author).unwrap();
516 let (wrap_l, _) = wrap_seal(&seal_l, &g, KIND_WRAP, wrap_at).unwrap();
517 let (wrap_s, _) = seal_and_wrap_signed(&as_signer(&author), author.public_key(), &rumor, SealForm::Encrypted, &g, KIND_WRAP, wrap_at, &[])
518 .await
519 .unwrap();
520
521 let opened_l = open_wrap(&wrap_l, &g).unwrap();
522 let opened_s = open_wrap(&wrap_s, &g).unwrap();
523 assert_eq!(opened_l.rumor_id, opened_s.rumor_id, "same inner rumor id via either seal path");
524 assert_eq!(opened_s.author, author.public_key(), "signer seal carries the author identity");
525 assert_eq!(opened_l.seal_form, opened_s.seal_form);
526 assert_eq!(opened_s.rumor.content, "parity");
527 }
528
529 #[test]
530 fn plaintext_seal_round_trip_carries_rumor_verbatim() {
531 let author = Keys::generate();
532 let wrap = send(&author, &group(), SealForm::Plaintext, "an edition", 1_686_840_217_000);
533 let opened = open_wrap(&wrap, &group()).unwrap();
534 assert_eq!(opened.seal_form, SealForm::Plaintext);
535 assert_eq!(opened.seal.content, opened.rumor.as_json());
537 }
538
539 #[test]
540 fn wrong_stream_key_cannot_open() {
541 let author = Keys::generate();
542 let wrap = send(&author, &group(), SealForm::Encrypted, "secret", 1_000);
543 let other = channel_group_key(&[8u8; 32], &chan(), Epoch(0));
544 assert!(matches!(open_wrap(&wrap, &other), Err(StreamError::WrongStream)));
546 }
547
548 #[test]
549 fn tampered_wrap_content_fails_the_mac() {
550 let author = Keys::generate();
551 let mut wrap = send(&author, &group(), SealForm::Encrypted, "x", 1_000);
552 let mut json: serde_json::Value = serde_json::from_str(&wrap.as_json()).unwrap();
553 let ct = json["content"].as_str().unwrap().to_string();
554 let mut bytes = ct.into_bytes();
557 bytes[20] = if bytes[20] == b'B' { b'C' } else { b'B' };
558 json["content"] = serde_json::Value::String(String::from_utf8(bytes).unwrap());
559 wrap = Event::from_json(json.to_string()).unwrap();
560 assert!(matches!(open_wrap(&wrap, &group()), Err(StreamError::Decrypt(_))));
561 }
562
563 #[test]
564 fn forged_seal_signature_is_rejected() {
565 let author = Keys::generate();
566 let impostor = Keys::generate();
567 let tags = channel_binding_tags(&chan(), Epoch(0));
568 let rumor = build_rumor_ms(kind::MESSAGE, author.public_key(), "hi", tags, 1_000);
569 let seal = build_seal(&rumor, SealForm::Encrypted, &group(), &impostor).unwrap();
572 let mut json: serde_json::Value = serde_json::from_str(&seal.as_json()).unwrap();
573 json["pubkey"] = serde_json::Value::String(author.public_key().to_hex());
574 let forged = Event::from_json(json.to_string());
577 let Ok(forged) = forged else { return }; let (wrap, _) = wrap_seal(&forged, &group(), KIND_WRAP, Timestamp::from_secs(1)).unwrap();
579 assert!(matches!(open_wrap(&wrap, &group()), Err(StreamError::BadSealSignature)));
580 }
581
582 #[test]
583 fn rumor_author_must_match_seal_author() {
584 let author = Keys::generate();
585 let other = Keys::generate();
586 let tags = channel_binding_tags(&chan(), Epoch(0));
587 let rumor = build_rumor_ms(kind::MESSAGE, other.public_key(), "spoof", tags, 1_000);
589 let seal = build_seal(&rumor, SealForm::Encrypted, &group(), &author).unwrap();
590 let (wrap, _) = wrap_seal(&seal, &group(), KIND_WRAP, Timestamp::from_secs(1)).unwrap();
591 assert!(matches!(open_wrap(&wrap, &group()), Err(StreamError::AuthorMismatch)));
592 }
593
594 #[test]
595 fn forged_rumor_id_is_rejected() {
596 let author = Keys::generate();
597 let tags = channel_binding_tags(&chan(), Epoch(0));
598 let rumor = build_rumor_ms(kind::MESSAGE, author.public_key(), "real", tags, 1_000);
599 let mut json: serde_json::Value = serde_json::from_str(&rumor.as_json()).unwrap();
600 json["id"] = serde_json::Value::String("00".repeat(32));
601 let forged_json = json.to_string();
602 let seal = EventBuilder::new(Kind::Custom(KIND_SEAL_PLAINTEXT), forged_json)
605 .custom_created_at(rumor.created_at)
606 .finalize(&author)
607 .unwrap();
608 let (wrap, _) = wrap_seal(&seal, &group(), KIND_WRAP, Timestamp::from_secs(1)).unwrap();
609 assert!(matches!(open_wrap(&wrap, &group()), Err(StreamError::BadRumorId)));
610 }
611
612 #[test]
613 fn ms_is_strict_absent_is_zero_invalid_is_dropped() {
614 let author = Keys::generate();
615 let rumor = build_rumor_secs(kind::MESSAGE, author.public_key(), "x", vec![], 1_000);
617 assert_eq!(resolve_ms_strict(&rumor).unwrap(), 1_000_000);
618 let ok = build_rumor_secs(
620 kind::MESSAGE,
621 author.public_key(),
622 "x",
623 vec![Tag::custom("ms", ["999".to_string()])],
624 1_000,
625 );
626 assert_eq!(resolve_ms_strict(&ok).unwrap(), 1_000_999);
627 for bad in ["1000", "-1", "12.5", "abc", "007", "", "+5", "+0", "+000", "+999"] {
631 let r = build_rumor_secs(
632 kind::MESSAGE,
633 author.public_key(),
634 "x",
635 vec![Tag::custom("ms", [bad.to_string()])],
636 1_000,
637 );
638 assert!(
639 matches!(resolve_ms_strict(&r), Err(StreamError::BadMs)),
640 "ms={bad:?} must be malformed"
641 );
642 }
643 }
644
645 #[test]
646 fn a_valueless_ms_tag_is_malformed_and_a_duplicate_takes_the_first() {
647 let author = Keys::generate();
650 let bare = build_rumor_secs(
651 kind::MESSAGE,
652 author.public_key(),
653 "x",
654 vec![Tag::custom("ms", Vec::<String>::new())],
655 1_000,
656 );
657 assert!(matches!(resolve_ms_strict(&bare), Err(StreamError::BadMs)));
658 let two = build_rumor_secs(
661 kind::MESSAGE,
662 author.public_key(),
663 "x",
664 vec![
665 Tag::custom("ms", Vec::<String>::new()),
666 Tag::custom("ms", ["5".to_string()]),
667 ],
668 1_000,
669 );
670 assert!(matches!(resolve_ms_strict(&two), Err(StreamError::BadMs)));
671 let two_valued = build_rumor_secs(
678 kind::MESSAGE,
679 author.public_key(),
680 "x",
681 vec![
682 Tag::custom("ms", ["1".to_string()]),
683 Tag::custom("ms", ["2".to_string()]),
684 ],
685 1_000,
686 );
687 assert_eq!(resolve_ms_strict(&two_valued).unwrap(), 1_000_001, "the FIRST ms wins");
688 }
689
690 #[test]
691 fn tag_numbers_are_spec_shaped_decimals() {
692 use crate::community::edition::is_tag_decimal;
693 for good in ["4", "0", "1099511627776"] {
695 assert!(is_tag_decimal(good), "{good:?} is decimal form");
696 }
697 for bad in ["04", "007", "00", "+4", "-4", "0x4", "1e2", " 4", "4 ", "", "4.0"] {
698 assert!(!is_tag_decimal(bad), "{bad:?} is NOT decimal form");
699 }
700 }
701
702 #[test]
703 fn a_citation_version_must_be_plain_digits() {
704 let eid = "ab".repeat(32);
709 let hash = "cd".repeat(32);
710 let cite = |v: &str| {
711 let tags = nostr_sdk::prelude::Tags::from_list(vec![Tag::custom(
712 "vac",
713 [eid.clone(), v.to_string(), hash.clone()],
714 )]);
715 crate::community::edition::AuthorityCitation::from_tags(&tags)
716 };
717 assert!(cite("5").is_some(), "a plain decimal is the shape the spec names");
718 assert!(cite("+5").is_none(), "a leading plus is not decimal form");
719 assert!(cite("").is_none());
720 assert!(cite("5x").is_none());
721 assert!(cite("05").is_none(), "no leading zeros (CORD-01 §5)");
722 }
723
724 #[test]
725 fn binding_rejects_splices_and_duplicates() {
726 let author = Keys::generate();
727 let tags = channel_binding_tags(&chan(), Epoch(0));
728 let rumor = build_rumor_ms(kind::MESSAGE, author.public_key(), "x", tags, 1_000);
729 assert!(matches!(
731 check_channel_binding(&rumor, &ChannelId([0xcd; 32]), Epoch(0)),
732 Err(StreamError::ChannelMismatch)
733 ));
734 assert!(matches!(
735 check_channel_binding(&rumor, &chan(), Epoch(1)),
736 Err(StreamError::EpochMismatch)
737 ));
738 let mut tags = channel_binding_tags(&chan(), Epoch(0));
740 tags.extend(channel_binding_tags(&chan(), Epoch(0)));
741 let dup = build_rumor_ms(kind::MESSAGE, author.public_key(), "x", tags, 1_000);
742 assert!(matches!(
743 check_channel_binding(&dup, &chan(), Epoch(0)),
744 Err(StreamError::DuplicateTag(_))
745 ));
746 let bare = build_rumor_ms(kind::MESSAGE, author.public_key(), "x", vec![], 1_000);
748 assert!(matches!(
749 check_channel_binding(&bare, &chan(), Epoch(0)),
750 Err(StreamError::MissingTag(_))
751 ));
752 }
753
754 #[test]
755 fn oversize_plaintext_is_refused_at_build_time() {
756 let author = Keys::generate();
757 let big = "x".repeat(NIP44_MAX_PLAINTEXT + 1);
758 let rumor = build_rumor_ms(kind::MESSAGE, author.public_key(), &big, vec![], 1_000);
759 assert!(matches!(
760 seal_content(&rumor, SealForm::Encrypted, &group()),
761 Err(StreamError::Oversize(_))
762 ));
763 }
764
765 #[test]
766 fn ephemeral_wrap_round_trips_and_bad_wrap_kind_rejects() {
767 let author = Keys::generate();
768 let tags = channel_binding_tags(&chan(), Epoch(0));
769 let rumor = build_rumor_ms(kind::TYPING, author.public_key(), "", tags, 5_000);
770 let seal = build_seal(&rumor, SealForm::Encrypted, &group(), &author).unwrap();
771 let (wrap, _) = wrap_seal(&seal, &group(), KIND_WRAP_EPHEMERAL, Timestamp::from_secs(5)).unwrap();
772 assert_eq!(wrap.kind.as_u16(), KIND_WRAP_EPHEMERAL);
773 assert_eq!(open_wrap(&wrap, &group()).unwrap().rumor.kind.as_u16(), kind::TYPING);
774 assert!(matches!(
775 wrap_seal(&seal, &group(), 1058, Timestamp::from_secs(5)),
776 Err(StreamError::BadWrapKind(1058))
777 ));
778 }
779
780 #[test]
781 fn rewrap_preserves_rumor_id_and_signature_across_epochs() {
782 let author = Keys::generate();
783 let wrap = send(&author, &group(), SealForm::Plaintext, "the head edition", 9_000);
784 let opened = open_wrap(&wrap, &group()).unwrap();
785
786 let next = channel_group_key(&[7u8; 32], &chan(), Epoch(1));
788 let (rewrapped, _) = rewrap_seal(&opened.seal, &next, Timestamp::from_secs(2_000)).unwrap();
789 let reopened = open_wrap(&rewrapped, &next).unwrap();
790
791 assert_eq!(reopened.rumor_id, opened.rumor_id, "rumor id survives the re-wrap");
792 assert_eq!(reopened.author, author.public_key(), "authorship survives");
793 assert_eq!(reopened.seal.sig, opened.seal.sig, "the original signature rides verbatim");
794 assert_ne!(reopened.wrapper_id, opened.wrapper_id, "outer identity differs per wrap");
795
796 let enc = send(&author, &group(), SealForm::Encrypted, "no", 9_000);
798 let enc_opened = open_wrap(&enc, &group()).unwrap();
799 assert!(matches!(
800 rewrap_seal(&enc_opened.seal, &next, Timestamp::from_secs(2_000)),
801 Err(StreamError::NotRewrappable)
802 ));
803 }
804
805 #[test]
806 fn wrap_p_tag_is_ephemeral_not_the_stream_or_author() {
807 let author = Keys::generate();
808 let g = group();
809 let tags = channel_binding_tags(&chan(), Epoch(0));
810 let rumor = build_rumor_ms(kind::MESSAGE, author.public_key(), "x", tags, 1_000);
811 let seal = build_seal(&rumor, SealForm::Encrypted, &g, &author).unwrap();
812 let (wrap, ephemeral) = wrap_seal(&seal, &g, KIND_WRAP, Timestamp::from_secs(1)).unwrap();
813 let p = wrap
814 .tags
815 .iter()
816 .find_map(|t| {
817 let s = t.as_slice();
818 (s.len() >= 2 && s[0] == "p").then(|| s[1].clone())
819 })
820 .expect("wrap carries a p tag");
821 assert_eq!(p, ephemeral.public_key().to_hex());
822 assert_ne!(p, g.pk_hex());
823 assert_ne!(p, author.public_key().to_hex());
824 }
825}