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 BadWrapSignature,
91 BadSealKind(u16),
93 BadSealSignature,
95 AuthorMismatch,
97 BadRumorId,
99 BadMs,
102 ChannelMismatch,
104 EpochMismatch,
106 MissingTag(&'static str),
107 DuplicateTag(&'static str),
109 NotRewrappable,
112}
113
114impl std::fmt::Display for StreamError {
115 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116 match self {
117 StreamError::Sign(e) => write!(f, "sign: {e}"),
118 StreamError::Encrypt(e) => write!(f, "encrypt: {e}"),
119 StreamError::Decrypt(e) => write!(f, "decrypt: {e}"),
120 StreamError::Parse(e) => write!(f, "parse: {e}"),
121 StreamError::Oversize(n) => write!(f, "plaintext {n} bytes exceeds NIP-44 cap"),
122 StreamError::BadWrapKind(k) => write!(f, "not a stream wrap kind: {k}"),
123 StreamError::WrongStream => write!(f, "wrap author is not this stream"),
124 StreamError::BadWrapSignature => write!(f, "write-restricted wrap signature invalid"),
125 StreamError::BadSealKind(k) => write!(f, "not a seal kind: {k}"),
126 StreamError::BadSealSignature => write!(f, "seal signature invalid"),
127 StreamError::AuthorMismatch => write!(f, "rumor pubkey != seal pubkey"),
128 StreamError::BadRumorId => write!(f, "rumor id != computed hash"),
129 StreamError::BadMs => write!(f, "ms tag outside 0..=999"),
130 StreamError::ChannelMismatch => write!(f, "channel-binding mismatch (splice)"),
131 StreamError::EpochMismatch => write!(f, "epoch-binding mismatch (splice/replay)"),
132 StreamError::MissingTag(t) => write!(f, "missing rumor tag: {t}"),
133 StreamError::DuplicateTag(t) => write!(f, "duplicate rumor tag: {t}"),
134 StreamError::NotRewrappable => write!(f, "only plaintext seals survive re-wrapping"),
135 }
136 }
137}
138
139impl std::error::Error for StreamError {}
140
141#[derive(Debug, Clone)]
143pub struct OpenedStream {
144 pub rumor: UnsignedEvent,
146 pub rumor_id: EventId,
148 pub author: PublicKey,
150 pub seal_form: SealForm,
152 pub seal: Event,
155 pub wrapper_id: EventId,
157 pub at_ms: u64,
159}
160
161pub fn split_ms(at_ms: u64) -> (u64, u16) {
165 (at_ms / 1000, (at_ms % 1000) as u16)
166}
167
168pub fn resolve_ms_strict(rumor: &UnsignedEvent) -> Result<u64, StreamError> {
179 let secs = rumor.created_at.as_secs();
180 let Some(raw) = rumor.tags.iter().find_map(|t| {
189 let s = t.as_slice();
190 (s.first().map(|k| k.as_str()) == Some(TAG_MS)).then(|| s.get(1).cloned())
191 }) else {
192 return Ok(secs.saturating_mul(1000));
193 };
194 let raw = raw.ok_or(StreamError::BadMs)?;
199 if raw.is_empty() || !raw.bytes().all(|b| b.is_ascii_digit()) {
200 return Err(StreamError::BadMs);
201 }
202 let n: u64 = raw.parse().map_err(|_| StreamError::BadMs)?;
203 if n > 999 || (raw.len() > 1 && raw.starts_with('0')) {
204 return Err(StreamError::BadMs);
205 }
206 Ok(secs.saturating_mul(1000).saturating_add(n))
207}
208
209pub fn build_rumor_ms(
214 kind: u16,
215 author: PublicKey,
216 content: &str,
217 mut tags: Vec<Tag>,
218 at_ms: u64,
219) -> UnsignedEvent {
220 let (secs, offset) = split_ms(at_ms);
221 tags.push(Tag::custom(TAG_MS, [offset.to_string()]));
222 build_rumor_secs(kind, author, content, tags, secs)
223}
224
225pub fn build_rumor_secs(
228 kind: u16,
229 author: PublicKey,
230 content: &str,
231 tags: Vec<Tag>,
232 at_secs: u64,
233) -> UnsignedEvent {
234 let mut rumor = EventBuilder::new(Kind::Custom(kind), content)
238 .tags(tags)
239 .custom_created_at(Timestamp::from_secs(at_secs))
240 .finalize_unsigned_with_id(author);
241 rumor.ensure_id();
242 rumor
243}
244
245pub fn seal_content(rumor: &UnsignedEvent, form: SealForm, group: &GroupKey) -> Result<String, StreamError> {
251 let json = rumor.as_json();
252 cap(json.len())?;
253 match form {
254 SealForm::Plaintext => Ok(json),
255 SealForm::Encrypted => {
256 let ct = crate::community::cipher::encrypt_with_random_nonce(group.conv_key(), json.as_bytes()).map_err(|e| StreamError::Encrypt(e.to_string()))?;
257 Ok(base64_simd::STANDARD.encode_to_string(&ct))
258 }
259 }
260}
261
262pub fn build_seal(rumor: &UnsignedEvent, form: SealForm, group: &GroupKey, author_keys: &Keys) -> Result<Event, StreamError> {
266 let content = seal_content(rumor, form, group)?;
267 EventBuilder::new(Kind::Custom(form.kind()), content)
268 .custom_created_at(rumor.created_at)
269 .finalize(author_keys)
270 .map_err(|e| StreamError::Sign(e.to_string()))
271}
272
273pub fn wrap_seal(seal: &Event, group: &GroupKey, wrap_kind: u16, wrap_at: Timestamp) -> Result<(Event, Keys), StreamError> {
282 wrap_seal_with_tags(seal, group, wrap_kind, wrap_at, &[])
283}
284
285pub fn wrap_seal_with_tags(
290 seal: &Event,
291 group: &GroupKey,
292 wrap_kind: u16,
293 wrap_at: Timestamp,
294 extra_tags: &[Tag],
295) -> Result<(Event, Keys), StreamError> {
296 if wrap_kind != KIND_WRAP && wrap_kind != KIND_WRAP_EPHEMERAL {
297 return Err(StreamError::BadWrapKind(wrap_kind));
298 }
299 let seal_json = seal.as_json();
300 cap(seal_json.len())?;
301 let ct = crate::community::cipher::encrypt_with_random_nonce(group.conv_key(), seal_json.as_bytes()).map_err(|e| StreamError::Encrypt(e.to_string()))?;
302 let ephemeral = Keys::generate();
303 let mut tags = vec![Tag::public_key(ephemeral.public_key())];
304 tags.extend_from_slice(extra_tags);
305 let wrap = EventBuilder::new(Kind::Custom(wrap_kind), base64_simd::STANDARD.encode_to_string(&ct))
306 .tags(tags)
307 .custom_created_at(wrap_at)
308 .finalize(group.keys())
309 .map_err(|e| StreamError::Sign(e.to_string()))?;
310 Ok((wrap, ephemeral))
311}
312
313pub async fn seal_and_wrap_signed<S: crate::signer::VectorSigner + ?Sized>(
319 signer: &S,
320 author: PublicKey,
321 rumor: &UnsignedEvent,
322 form: SealForm,
323 group: &GroupKey,
324 wrap_kind: u16,
325 wrap_at: Timestamp,
326 extra_tags: &[Tag],
327) -> Result<(Event, Keys), StreamError> {
328 let content = seal_content(rumor, form, group)?;
329 let unsigned = EventBuilder::new(Kind::Custom(form.kind()), content)
330 .custom_created_at(rumor.created_at)
331 .finalize_unsigned_with_id(author);
332 let seal = signer
333 .sign_event_async(unsigned)
334 .await
335 .map_err(|e| StreamError::Sign(e.to_string()))?;
336 wrap_seal_with_tags(&seal, group, wrap_kind, wrap_at, extra_tags)
337}
338
339pub fn rewrap_seal(seal: &Event, new_group: &GroupKey, wrap_at: Timestamp) -> Result<(Event, Keys), StreamError> {
344 if seal.kind.as_u16() != KIND_SEAL_PLAINTEXT {
345 return Err(StreamError::NotRewrappable);
346 }
347 wrap_seal(seal, new_group, KIND_WRAP, wrap_at)
348}
349
350pub fn open_wrap(wrap: &Event, group: &GroupKey) -> Result<OpenedStream, StreamError> {
361 open_wrap_at(wrap, &group.pk(), group.conv_key(), false)
362}
363
364pub fn open_wrap_at(
372 wrap: &Event,
373 address: &PublicKey,
374 conv_key: &nostr_sdk::prelude::nip44::v2::ConversationKey,
375 verify_wrap_sig: bool,
376) -> Result<OpenedStream, StreamError> {
377 let wrap_kind = wrap.kind.as_u16();
378 if wrap_kind != KIND_WRAP && wrap_kind != KIND_WRAP_EPHEMERAL {
379 return Err(StreamError::BadWrapKind(wrap_kind));
380 }
381 if wrap.pubkey != *address {
382 return Err(StreamError::WrongStream);
383 }
384 if verify_wrap_sig && wrap.verify().is_err() {
385 return Err(StreamError::BadWrapSignature);
386 }
387
388 let seal_json = open_nip44(conv_key, &wrap.content)?;
389 let seal: Event = Event::from_json(&seal_json).map_err(|e| StreamError::Parse(e.to_string()))?;
390 let seal_form = SealForm::from_kind(seal.kind.as_u16()).ok_or(StreamError::BadSealKind(seal.kind.as_u16()))?;
391 seal.verify().map_err(|_| StreamError::BadSealSignature)?;
392
393 let rumor_json = match seal_form {
394 SealForm::Plaintext => seal.content.clone(),
395 SealForm::Encrypted => open_nip44(conv_key, &seal.content)?,
396 };
397 let mut rumor: UnsignedEvent = UnsignedEvent::from_json(rumor_json.as_bytes()).map_err(|e| StreamError::Parse(e.to_string()))?;
398
399 if rumor.pubkey != seal.pubkey {
400 return Err(StreamError::AuthorMismatch);
401 }
402 let computed = EventId::compute(&rumor.pubkey, &rumor.created_at, &rumor.kind, &rumor.tags, &rumor.content);
406 if let Some(claimed) = rumor.id {
407 if claimed != computed {
408 return Err(StreamError::BadRumorId);
409 }
410 }
411 rumor.id = Some(computed);
412 let at_ms = resolve_ms_strict(&rumor)?;
413
414 Ok(OpenedStream {
415 rumor_id: computed,
416 author: seal.pubkey,
417 seal_form,
418 seal,
419 wrapper_id: wrap.id,
420 at_ms,
421 rumor,
422 })
423}
424
425pub fn check_channel_binding(rumor: &UnsignedEvent, channel_id: &ChannelId, epoch: Epoch) -> Result<(), StreamError> {
429 match unique_tag_unsigned(rumor, TAG_CHANNEL)? {
430 Some(c) if c == channel_id.to_hex() => {}
431 Some(_) => return Err(StreamError::ChannelMismatch),
432 None => return Err(StreamError::MissingTag(TAG_CHANNEL)),
433 }
434 match unique_tag_unsigned(rumor, TAG_EPOCH)? {
435 Some(e) if e == epoch.0.to_string() => {}
436 Some(_) => return Err(StreamError::EpochMismatch),
437 None => return Err(StreamError::MissingTag(TAG_EPOCH)),
438 }
439 Ok(())
440}
441
442pub fn channel_binding_tags(channel_id: &ChannelId, epoch: Epoch) -> Vec<Tag> {
444 vec![
445 Tag::custom(TAG_CHANNEL, [channel_id.to_hex()]),
446 Tag::custom(TAG_EPOCH, [epoch.0.to_string()]),
447 ]
448}
449
450fn cap(len: usize) -> Result<(), StreamError> {
453 if len > NIP44_MAX_PLAINTEXT {
454 return Err(StreamError::Oversize(len));
455 }
456 Ok(())
457}
458
459fn open_nip44(conv_key: &ConversationKey, content_b64: &str) -> Result<String, StreamError> {
460 let ct = base64_simd::STANDARD
461 .decode_to_vec(content_b64.as_bytes())
462 .map_err(|e| StreamError::Decrypt(e.to_string()))?;
463 let pt = decrypt_to_bytes(conv_key, &ct).map_err(|e| StreamError::Decrypt(e.to_string()))?;
464 String::from_utf8(pt).map_err(|e| StreamError::Parse(e.to_string()))
465}
466
467fn unique_tag_unsigned(rumor: &UnsignedEvent, name: &'static str) -> Result<Option<String>, StreamError> {
471 let mut found: Option<String> = None;
472 for t in rumor.tags.iter() {
473 let s = t.as_slice();
474 if s.len() >= 2 && s[0] == name {
475 if found.is_some() {
476 return Err(StreamError::DuplicateTag(name));
477 }
478 found = Some(s[1].clone());
479 }
480 }
481 Ok(found)
482}
483
484#[cfg(test)]
485mod tests {
486
487 fn as_signer(k: &Keys) -> crate::signer::ActiveSigner {
491 crate::signer::ActiveSigner::Keys(k.clone())
492 }
493 use super::super::super::{ChannelId, Epoch};
494 use super::super::derive::channel_group_key;
495 use super::super::kind;
496 use super::*;
497
498 fn group() -> GroupKey {
499 channel_group_key(&[7u8; 32], &chan(), Epoch(0))
500 }
501
502 fn chan() -> ChannelId {
503 ChannelId([0xabu8; 32])
504 }
505
506 fn send(author: &Keys, group: &GroupKey, form: SealForm, content: &str, at_ms: u64) -> Event {
507 let tags = channel_binding_tags(&chan(), Epoch(0));
508 let rumor = build_rumor_ms(kind::MESSAGE, author.public_key(), content, tags, at_ms);
509 let seal = build_seal(&rumor, form, group, author).unwrap();
510 wrap_seal(&seal, group, KIND_WRAP, Timestamp::from_secs(1_700_000_000)).unwrap().0
511 }
512
513 #[test]
514 fn encrypted_round_trip_preserves_author_content_and_ms() {
515 let author = Keys::generate();
516 let wrap = send(&author, &group(), SealForm::Encrypted, "Hey chat!", 1_686_840_217_417);
517 assert_eq!(wrap.kind.as_u16(), KIND_WRAP);
518 assert_eq!(wrap.pubkey, group().pk(), "wrap is signed by the stream key");
519
520 let opened = open_wrap(&wrap, &group()).unwrap();
521 assert_eq!(opened.author, author.public_key());
522 assert_eq!(opened.rumor.content, "Hey chat!");
523 assert_eq!(opened.at_ms, 1_686_840_217_417);
524 assert_eq!(opened.seal_form, SealForm::Encrypted);
525 check_channel_binding(&opened.rumor, &chan(), Epoch(0)).unwrap();
526 }
527
528 #[tokio::test]
529 async fn signer_seal_opens_identically_to_local_seal() {
530 let author = Keys::generate();
534 let g = group();
535 let tags = channel_binding_tags(&chan(), Epoch(0));
536 let rumor = build_rumor_ms(kind::MESSAGE, author.public_key(), "parity", tags, 1_686_840_217_000);
537 let wrap_at = Timestamp::from_secs(1_686_840_217);
538
539 let seal_l = build_seal(&rumor, SealForm::Encrypted, &g, &author).unwrap();
540 let (wrap_l, _) = wrap_seal(&seal_l, &g, KIND_WRAP, wrap_at).unwrap();
541 let (wrap_s, _) = seal_and_wrap_signed(&as_signer(&author), author.public_key(), &rumor, SealForm::Encrypted, &g, KIND_WRAP, wrap_at, &[])
542 .await
543 .unwrap();
544
545 let opened_l = open_wrap(&wrap_l, &g).unwrap();
546 let opened_s = open_wrap(&wrap_s, &g).unwrap();
547 assert_eq!(opened_l.rumor_id, opened_s.rumor_id, "same inner rumor id via either seal path");
548 assert_eq!(opened_s.author, author.public_key(), "signer seal carries the author identity");
549 assert_eq!(opened_l.seal_form, opened_s.seal_form);
550 assert_eq!(opened_s.rumor.content, "parity");
551 }
552
553 #[test]
554 fn plaintext_seal_round_trip_carries_rumor_verbatim() {
555 let author = Keys::generate();
556 let wrap = send(&author, &group(), SealForm::Plaintext, "an edition", 1_686_840_217_000);
557 let opened = open_wrap(&wrap, &group()).unwrap();
558 assert_eq!(opened.seal_form, SealForm::Plaintext);
559 assert_eq!(opened.seal.content, opened.rumor.as_json());
561 }
562
563 #[test]
564 fn wrong_stream_key_cannot_open() {
565 let author = Keys::generate();
566 let wrap = send(&author, &group(), SealForm::Encrypted, "secret", 1_000);
567 let other = channel_group_key(&[8u8; 32], &chan(), Epoch(0));
568 assert!(matches!(open_wrap(&wrap, &other), Err(StreamError::WrongStream)));
570 }
571
572 #[test]
573 fn tampered_wrap_content_fails_the_mac() {
574 let author = Keys::generate();
575 let mut wrap = send(&author, &group(), SealForm::Encrypted, "x", 1_000);
576 let mut json: serde_json::Value = serde_json::from_str(&wrap.as_json()).unwrap();
577 let ct = json["content"].as_str().unwrap().to_string();
578 let mut bytes = ct.into_bytes();
581 bytes[20] = if bytes[20] == b'B' { b'C' } else { b'B' };
582 json["content"] = serde_json::Value::String(String::from_utf8(bytes).unwrap());
583 wrap = Event::from_json(json.to_string()).unwrap();
584 assert!(matches!(open_wrap(&wrap, &group()), Err(StreamError::Decrypt(_))));
585 }
586
587 #[test]
588 fn forged_seal_signature_is_rejected() {
589 let author = Keys::generate();
590 let impostor = Keys::generate();
591 let tags = channel_binding_tags(&chan(), Epoch(0));
592 let rumor = build_rumor_ms(kind::MESSAGE, author.public_key(), "hi", tags, 1_000);
593 let seal = build_seal(&rumor, SealForm::Encrypted, &group(), &impostor).unwrap();
596 let mut json: serde_json::Value = serde_json::from_str(&seal.as_json()).unwrap();
597 json["pubkey"] = serde_json::Value::String(author.public_key().to_hex());
598 let forged = Event::from_json(json.to_string());
601 let Ok(forged) = forged else { return }; let (wrap, _) = wrap_seal(&forged, &group(), KIND_WRAP, Timestamp::from_secs(1)).unwrap();
603 assert!(matches!(open_wrap(&wrap, &group()), Err(StreamError::BadSealSignature)));
604 }
605
606 #[test]
607 fn rumor_author_must_match_seal_author() {
608 let author = Keys::generate();
609 let other = Keys::generate();
610 let tags = channel_binding_tags(&chan(), Epoch(0));
611 let rumor = build_rumor_ms(kind::MESSAGE, other.public_key(), "spoof", tags, 1_000);
613 let seal = build_seal(&rumor, SealForm::Encrypted, &group(), &author).unwrap();
614 let (wrap, _) = wrap_seal(&seal, &group(), KIND_WRAP, Timestamp::from_secs(1)).unwrap();
615 assert!(matches!(open_wrap(&wrap, &group()), Err(StreamError::AuthorMismatch)));
616 }
617
618 #[test]
619 fn forged_rumor_id_is_rejected() {
620 let author = Keys::generate();
621 let tags = channel_binding_tags(&chan(), Epoch(0));
622 let rumor = build_rumor_ms(kind::MESSAGE, author.public_key(), "real", tags, 1_000);
623 let mut json: serde_json::Value = serde_json::from_str(&rumor.as_json()).unwrap();
624 json["id"] = serde_json::Value::String("00".repeat(32));
625 let forged_json = json.to_string();
626 let seal = EventBuilder::new(Kind::Custom(KIND_SEAL_PLAINTEXT), forged_json)
629 .custom_created_at(rumor.created_at)
630 .finalize(&author)
631 .unwrap();
632 let (wrap, _) = wrap_seal(&seal, &group(), KIND_WRAP, Timestamp::from_secs(1)).unwrap();
633 assert!(matches!(open_wrap(&wrap, &group()), Err(StreamError::BadRumorId)));
634 }
635
636 #[test]
637 fn ms_is_strict_absent_is_zero_invalid_is_dropped() {
638 let author = Keys::generate();
639 let rumor = build_rumor_secs(kind::MESSAGE, author.public_key(), "x", vec![], 1_000);
641 assert_eq!(resolve_ms_strict(&rumor).unwrap(), 1_000_000);
642 let ok = build_rumor_secs(
644 kind::MESSAGE,
645 author.public_key(),
646 "x",
647 vec![Tag::custom("ms", ["999".to_string()])],
648 1_000,
649 );
650 assert_eq!(resolve_ms_strict(&ok).unwrap(), 1_000_999);
651 for bad in ["1000", "-1", "12.5", "abc", "007", "", "+5", "+0", "+000", "+999"] {
655 let r = build_rumor_secs(
656 kind::MESSAGE,
657 author.public_key(),
658 "x",
659 vec![Tag::custom("ms", [bad.to_string()])],
660 1_000,
661 );
662 assert!(
663 matches!(resolve_ms_strict(&r), Err(StreamError::BadMs)),
664 "ms={bad:?} must be malformed"
665 );
666 }
667 }
668
669 #[test]
670 fn a_valueless_ms_tag_is_malformed_and_a_duplicate_takes_the_first() {
671 let author = Keys::generate();
674 let bare = build_rumor_secs(
675 kind::MESSAGE,
676 author.public_key(),
677 "x",
678 vec![Tag::custom("ms", Vec::<String>::new())],
679 1_000,
680 );
681 assert!(matches!(resolve_ms_strict(&bare), Err(StreamError::BadMs)));
682 let two = build_rumor_secs(
685 kind::MESSAGE,
686 author.public_key(),
687 "x",
688 vec![
689 Tag::custom("ms", Vec::<String>::new()),
690 Tag::custom("ms", ["5".to_string()]),
691 ],
692 1_000,
693 );
694 assert!(matches!(resolve_ms_strict(&two), Err(StreamError::BadMs)));
695 let two_valued = build_rumor_secs(
702 kind::MESSAGE,
703 author.public_key(),
704 "x",
705 vec![
706 Tag::custom("ms", ["1".to_string()]),
707 Tag::custom("ms", ["2".to_string()]),
708 ],
709 1_000,
710 );
711 assert_eq!(resolve_ms_strict(&two_valued).unwrap(), 1_000_001, "the FIRST ms wins");
712 }
713
714 #[test]
715 fn tag_numbers_are_spec_shaped_decimals() {
716 use crate::community::edition::is_tag_decimal;
717 for good in ["4", "0", "1099511627776"] {
719 assert!(is_tag_decimal(good), "{good:?} is decimal form");
720 }
721 for bad in ["04", "007", "00", "+4", "-4", "0x4", "1e2", " 4", "4 ", "", "4.0"] {
722 assert!(!is_tag_decimal(bad), "{bad:?} is NOT decimal form");
723 }
724 }
725
726 #[test]
727 fn a_citation_version_must_be_plain_digits() {
728 let eid = "ab".repeat(32);
733 let hash = "cd".repeat(32);
734 let cite = |v: &str| {
735 let tags = nostr_sdk::prelude::Tags::from_list(vec![Tag::custom(
736 "vac",
737 [eid.clone(), v.to_string(), hash.clone()],
738 )]);
739 crate::community::edition::AuthorityCitation::from_tags(&tags)
740 };
741 assert!(cite("5").is_some(), "a plain decimal is the shape the spec names");
742 assert!(cite("+5").is_none(), "a leading plus is not decimal form");
743 assert!(cite("").is_none());
744 assert!(cite("5x").is_none());
745 assert!(cite("05").is_none(), "no leading zeros (CORD-01 §5)");
746 }
747
748 #[test]
749 fn binding_rejects_splices_and_duplicates() {
750 let author = Keys::generate();
751 let tags = channel_binding_tags(&chan(), Epoch(0));
752 let rumor = build_rumor_ms(kind::MESSAGE, author.public_key(), "x", tags, 1_000);
753 assert!(matches!(
755 check_channel_binding(&rumor, &ChannelId([0xcd; 32]), Epoch(0)),
756 Err(StreamError::ChannelMismatch)
757 ));
758 assert!(matches!(
759 check_channel_binding(&rumor, &chan(), Epoch(1)),
760 Err(StreamError::EpochMismatch)
761 ));
762 let mut tags = channel_binding_tags(&chan(), Epoch(0));
764 tags.extend(channel_binding_tags(&chan(), Epoch(0)));
765 let dup = build_rumor_ms(kind::MESSAGE, author.public_key(), "x", tags, 1_000);
766 assert!(matches!(
767 check_channel_binding(&dup, &chan(), Epoch(0)),
768 Err(StreamError::DuplicateTag(_))
769 ));
770 let bare = build_rumor_ms(kind::MESSAGE, author.public_key(), "x", vec![], 1_000);
772 assert!(matches!(
773 check_channel_binding(&bare, &chan(), Epoch(0)),
774 Err(StreamError::MissingTag(_))
775 ));
776 }
777
778 #[test]
779 fn oversize_plaintext_is_refused_at_build_time() {
780 let author = Keys::generate();
781 let big = "x".repeat(NIP44_MAX_PLAINTEXT + 1);
782 let rumor = build_rumor_ms(kind::MESSAGE, author.public_key(), &big, vec![], 1_000);
783 assert!(matches!(
784 seal_content(&rumor, SealForm::Encrypted, &group()),
785 Err(StreamError::Oversize(_))
786 ));
787 }
788
789 #[test]
790 fn ephemeral_wrap_round_trips_and_bad_wrap_kind_rejects() {
791 let author = Keys::generate();
792 let tags = channel_binding_tags(&chan(), Epoch(0));
793 let rumor = build_rumor_ms(kind::TYPING, author.public_key(), "", tags, 5_000);
794 let seal = build_seal(&rumor, SealForm::Encrypted, &group(), &author).unwrap();
795 let (wrap, _) = wrap_seal(&seal, &group(), KIND_WRAP_EPHEMERAL, Timestamp::from_secs(5)).unwrap();
796 assert_eq!(wrap.kind.as_u16(), KIND_WRAP_EPHEMERAL);
797 assert_eq!(open_wrap(&wrap, &group()).unwrap().rumor.kind.as_u16(), kind::TYPING);
798 assert!(matches!(
799 wrap_seal(&seal, &group(), 1058, Timestamp::from_secs(5)),
800 Err(StreamError::BadWrapKind(1058))
801 ));
802 }
803
804 #[test]
805 fn rewrap_preserves_rumor_id_and_signature_across_epochs() {
806 let author = Keys::generate();
807 let wrap = send(&author, &group(), SealForm::Plaintext, "the head edition", 9_000);
808 let opened = open_wrap(&wrap, &group()).unwrap();
809
810 let next = channel_group_key(&[7u8; 32], &chan(), Epoch(1));
812 let (rewrapped, _) = rewrap_seal(&opened.seal, &next, Timestamp::from_secs(2_000)).unwrap();
813 let reopened = open_wrap(&rewrapped, &next).unwrap();
814
815 assert_eq!(reopened.rumor_id, opened.rumor_id, "rumor id survives the re-wrap");
816 assert_eq!(reopened.author, author.public_key(), "authorship survives");
817 assert_eq!(reopened.seal.sig, opened.seal.sig, "the original signature rides verbatim");
818 assert_ne!(reopened.wrapper_id, opened.wrapper_id, "outer identity differs per wrap");
819
820 let enc = send(&author, &group(), SealForm::Encrypted, "no", 9_000);
822 let enc_opened = open_wrap(&enc, &group()).unwrap();
823 assert!(matches!(
824 rewrap_seal(&enc_opened.seal, &next, Timestamp::from_secs(2_000)),
825 Err(StreamError::NotRewrappable)
826 ));
827 }
828
829 #[test]
830 fn wrap_p_tag_is_ephemeral_not_the_stream_or_author() {
831 let author = Keys::generate();
832 let g = group();
833 let tags = channel_binding_tags(&chan(), Epoch(0));
834 let rumor = build_rumor_ms(kind::MESSAGE, author.public_key(), "x", tags, 1_000);
835 let seal = build_seal(&rumor, SealForm::Encrypted, &g, &author).unwrap();
836 let (wrap, ephemeral) = wrap_seal(&seal, &g, KIND_WRAP, Timestamp::from_secs(1)).unwrap();
837 let p = wrap
838 .tags
839 .iter()
840 .find_map(|t| {
841 let s = t.as_slice();
842 (s.len() >= 2 && s[0] == "p").then(|| s[1].clone())
843 })
844 .expect("wrap carries a p tag");
845 assert_eq!(p, ephemeral.public_key().to_hex());
846 assert_ne!(p, g.pk_hex());
847 assert_ne!(p, author.public_key().to_hex());
848 }
849}