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
313#[allow(clippy::too_many_arguments)]
319pub async fn seal_and_wrap_signed<S: crate::signer::VectorSigner + ?Sized>(
320 signer: &S,
321 author: PublicKey,
322 rumor: &UnsignedEvent,
323 form: SealForm,
324 group: &GroupKey,
325 wrap_kind: u16,
326 wrap_at: Timestamp,
327 extra_tags: &[Tag],
328) -> Result<(Event, Keys), StreamError> {
329 let content = seal_content(rumor, form, group)?;
330 let unsigned = EventBuilder::new(Kind::Custom(form.kind()), content)
331 .custom_created_at(rumor.created_at)
332 .finalize_unsigned_with_id(author);
333 let seal = signer
334 .sign_event_async(unsigned)
335 .await
336 .map_err(|e| StreamError::Sign(e.to_string()))?;
337 wrap_seal_with_tags(&seal, group, wrap_kind, wrap_at, extra_tags)
338}
339
340pub fn rewrap_seal(seal: &Event, new_group: &GroupKey, wrap_at: Timestamp) -> Result<(Event, Keys), StreamError> {
345 if seal.kind.as_u16() != KIND_SEAL_PLAINTEXT {
346 return Err(StreamError::NotRewrappable);
347 }
348 wrap_seal(seal, new_group, KIND_WRAP, wrap_at)
349}
350
351pub fn open_wrap(wrap: &Event, group: &GroupKey) -> Result<OpenedStream, StreamError> {
362 open_wrap_at(wrap, &group.pk(), group.conv_key(), false)
363}
364
365pub fn open_wrap_at(
373 wrap: &Event,
374 address: &PublicKey,
375 conv_key: &nostr_sdk::prelude::nip44::v2::ConversationKey,
376 verify_wrap_sig: bool,
377) -> Result<OpenedStream, StreamError> {
378 let wrap_kind = wrap.kind.as_u16();
379 if wrap_kind != KIND_WRAP && wrap_kind != KIND_WRAP_EPHEMERAL {
380 return Err(StreamError::BadWrapKind(wrap_kind));
381 }
382 if wrap.pubkey != *address {
383 return Err(StreamError::WrongStream);
384 }
385 if verify_wrap_sig && wrap.verify().is_err() {
386 return Err(StreamError::BadWrapSignature);
387 }
388
389 let seal_json = open_nip44(conv_key, &wrap.content)?;
390 let seal: Event = Event::from_json(&seal_json).map_err(|e| StreamError::Parse(e.to_string()))?;
391 let seal_form = SealForm::from_kind(seal.kind.as_u16()).ok_or(StreamError::BadSealKind(seal.kind.as_u16()))?;
392 seal.verify().map_err(|_| StreamError::BadSealSignature)?;
393
394 let rumor_json = match seal_form {
395 SealForm::Plaintext => seal.content.clone(),
396 SealForm::Encrypted => open_nip44(conv_key, &seal.content)?,
397 };
398 let mut rumor: UnsignedEvent = UnsignedEvent::from_json(rumor_json.as_bytes()).map_err(|e| StreamError::Parse(e.to_string()))?;
399
400 if rumor.pubkey != seal.pubkey {
401 return Err(StreamError::AuthorMismatch);
402 }
403 let computed = EventId::compute(&rumor.pubkey, &rumor.created_at, &rumor.kind, &rumor.tags, &rumor.content);
407 if let Some(claimed) = rumor.id {
408 if claimed != computed {
409 return Err(StreamError::BadRumorId);
410 }
411 }
412 rumor.id = Some(computed);
413 let at_ms = resolve_ms_strict(&rumor)?;
414
415 Ok(OpenedStream {
416 rumor_id: computed,
417 author: seal.pubkey,
418 seal_form,
419 seal,
420 wrapper_id: wrap.id,
421 at_ms,
422 rumor,
423 })
424}
425
426pub fn check_channel_binding(rumor: &UnsignedEvent, channel_id: &ChannelId, epoch: Epoch) -> Result<(), StreamError> {
430 match unique_tag_unsigned(rumor, TAG_CHANNEL)? {
431 Some(c) if c == channel_id.to_hex() => {}
432 Some(_) => return Err(StreamError::ChannelMismatch),
433 None => return Err(StreamError::MissingTag(TAG_CHANNEL)),
434 }
435 match unique_tag_unsigned(rumor, TAG_EPOCH)? {
436 Some(e) if e == epoch.0.to_string() => {}
437 Some(_) => return Err(StreamError::EpochMismatch),
438 None => return Err(StreamError::MissingTag(TAG_EPOCH)),
439 }
440 Ok(())
441}
442
443pub fn channel_binding_tags(channel_id: &ChannelId, epoch: Epoch) -> Vec<Tag> {
445 vec![
446 Tag::custom(TAG_CHANNEL, [channel_id.to_hex()]),
447 Tag::custom(TAG_EPOCH, [epoch.0.to_string()]),
448 ]
449}
450
451fn cap(len: usize) -> Result<(), StreamError> {
454 if len > NIP44_MAX_PLAINTEXT {
455 return Err(StreamError::Oversize(len));
456 }
457 Ok(())
458}
459
460fn open_nip44(conv_key: &ConversationKey, content_b64: &str) -> Result<String, StreamError> {
461 let ct = base64_simd::STANDARD
462 .decode_to_vec(content_b64.as_bytes())
463 .map_err(|e| StreamError::Decrypt(e.to_string()))?;
464 let pt = decrypt_to_bytes(conv_key, &ct).map_err(|e| StreamError::Decrypt(e.to_string()))?;
465 String::from_utf8(pt).map_err(|e| StreamError::Parse(e.to_string()))
466}
467
468fn unique_tag_unsigned(rumor: &UnsignedEvent, name: &'static str) -> Result<Option<String>, StreamError> {
472 let mut found: Option<String> = None;
473 for t in rumor.tags.iter() {
474 let s = t.as_slice();
475 if s.len() >= 2 && s[0] == name {
476 if found.is_some() {
477 return Err(StreamError::DuplicateTag(name));
478 }
479 found = Some(s[1].clone());
480 }
481 }
482 Ok(found)
483}
484
485#[cfg(test)]
486mod tests {
487
488 fn as_signer(k: &Keys) -> crate::signer::ActiveSigner {
492 crate::signer::ActiveSigner::Keys(k.clone())
493 }
494 use super::super::super::{ChannelId, Epoch};
495 use super::super::derive::channel_group_key;
496 use super::super::kind;
497 use super::*;
498
499 fn group() -> GroupKey {
500 channel_group_key(&[7u8; 32], &chan(), Epoch(0))
501 }
502
503 fn chan() -> ChannelId {
504 ChannelId([0xabu8; 32])
505 }
506
507 fn send(author: &Keys, group: &GroupKey, form: SealForm, content: &str, at_ms: u64) -> Event {
508 let tags = channel_binding_tags(&chan(), Epoch(0));
509 let rumor = build_rumor_ms(kind::MESSAGE, author.public_key(), content, tags, at_ms);
510 let seal = build_seal(&rumor, form, group, author).unwrap();
511 wrap_seal(&seal, group, KIND_WRAP, Timestamp::from_secs(1_700_000_000)).unwrap().0
512 }
513
514 #[test]
515 fn encrypted_round_trip_preserves_author_content_and_ms() {
516 let author = Keys::generate();
517 let wrap = send(&author, &group(), SealForm::Encrypted, "Hey chat!", 1_686_840_217_417);
518 assert_eq!(wrap.kind.as_u16(), KIND_WRAP);
519 assert_eq!(wrap.pubkey, group().pk(), "wrap is signed by the stream key");
520
521 let opened = open_wrap(&wrap, &group()).unwrap();
522 assert_eq!(opened.author, author.public_key());
523 assert_eq!(opened.rumor.content, "Hey chat!");
524 assert_eq!(opened.at_ms, 1_686_840_217_417);
525 assert_eq!(opened.seal_form, SealForm::Encrypted);
526 check_channel_binding(&opened.rumor, &chan(), Epoch(0)).unwrap();
527 }
528
529 #[tokio::test]
530 async fn signer_seal_opens_identically_to_local_seal() {
531 let author = Keys::generate();
535 let g = group();
536 let tags = channel_binding_tags(&chan(), Epoch(0));
537 let rumor = build_rumor_ms(kind::MESSAGE, author.public_key(), "parity", tags, 1_686_840_217_000);
538 let wrap_at = Timestamp::from_secs(1_686_840_217);
539
540 let seal_l = build_seal(&rumor, SealForm::Encrypted, &g, &author).unwrap();
541 let (wrap_l, _) = wrap_seal(&seal_l, &g, KIND_WRAP, wrap_at).unwrap();
542 let (wrap_s, _) = seal_and_wrap_signed(&as_signer(&author), author.public_key(), &rumor, SealForm::Encrypted, &g, KIND_WRAP, wrap_at, &[])
543 .await
544 .unwrap();
545
546 let opened_l = open_wrap(&wrap_l, &g).unwrap();
547 let opened_s = open_wrap(&wrap_s, &g).unwrap();
548 assert_eq!(opened_l.rumor_id, opened_s.rumor_id, "same inner rumor id via either seal path");
549 assert_eq!(opened_s.author, author.public_key(), "signer seal carries the author identity");
550 assert_eq!(opened_l.seal_form, opened_s.seal_form);
551 assert_eq!(opened_s.rumor.content, "parity");
552 }
553
554 #[test]
555 fn plaintext_seal_round_trip_carries_rumor_verbatim() {
556 let author = Keys::generate();
557 let wrap = send(&author, &group(), SealForm::Plaintext, "an edition", 1_686_840_217_000);
558 let opened = open_wrap(&wrap, &group()).unwrap();
559 assert_eq!(opened.seal_form, SealForm::Plaintext);
560 assert_eq!(opened.seal.content, opened.rumor.as_json());
562 }
563
564 #[test]
565 fn wrong_stream_key_cannot_open() {
566 let author = Keys::generate();
567 let wrap = send(&author, &group(), SealForm::Encrypted, "secret", 1_000);
568 let other = channel_group_key(&[8u8; 32], &chan(), Epoch(0));
569 assert!(matches!(open_wrap(&wrap, &other), Err(StreamError::WrongStream)));
571 }
572
573 #[test]
574 fn tampered_wrap_content_fails_the_mac() {
575 let author = Keys::generate();
576 let mut wrap = send(&author, &group(), SealForm::Encrypted, "x", 1_000);
577 let mut json: serde_json::Value = serde_json::from_str(&wrap.as_json()).unwrap();
578 let ct = json["content"].as_str().unwrap().to_string();
579 let mut bytes = ct.into_bytes();
582 bytes[20] = if bytes[20] == b'B' { b'C' } else { b'B' };
583 json["content"] = serde_json::Value::String(String::from_utf8(bytes).unwrap());
584 wrap = Event::from_json(json.to_string()).unwrap();
585 assert!(matches!(open_wrap(&wrap, &group()), Err(StreamError::Decrypt(_))));
586 }
587
588 #[test]
589 fn forged_seal_signature_is_rejected() {
590 let author = Keys::generate();
591 let impostor = Keys::generate();
592 let tags = channel_binding_tags(&chan(), Epoch(0));
593 let rumor = build_rumor_ms(kind::MESSAGE, author.public_key(), "hi", tags, 1_000);
594 let seal = build_seal(&rumor, SealForm::Encrypted, &group(), &impostor).unwrap();
597 let mut json: serde_json::Value = serde_json::from_str(&seal.as_json()).unwrap();
598 json["pubkey"] = serde_json::Value::String(author.public_key().to_hex());
599 let forged = Event::from_json(json.to_string());
602 let Ok(forged) = forged else { return }; let (wrap, _) = wrap_seal(&forged, &group(), KIND_WRAP, Timestamp::from_secs(1)).unwrap();
604 assert!(matches!(open_wrap(&wrap, &group()), Err(StreamError::BadSealSignature)));
605 }
606
607 #[test]
608 fn rumor_author_must_match_seal_author() {
609 let author = Keys::generate();
610 let other = Keys::generate();
611 let tags = channel_binding_tags(&chan(), Epoch(0));
612 let rumor = build_rumor_ms(kind::MESSAGE, other.public_key(), "spoof", tags, 1_000);
614 let seal = build_seal(&rumor, SealForm::Encrypted, &group(), &author).unwrap();
615 let (wrap, _) = wrap_seal(&seal, &group(), KIND_WRAP, Timestamp::from_secs(1)).unwrap();
616 assert!(matches!(open_wrap(&wrap, &group()), Err(StreamError::AuthorMismatch)));
617 }
618
619 #[test]
620 fn forged_rumor_id_is_rejected() {
621 let author = Keys::generate();
622 let tags = channel_binding_tags(&chan(), Epoch(0));
623 let rumor = build_rumor_ms(kind::MESSAGE, author.public_key(), "real", tags, 1_000);
624 let mut json: serde_json::Value = serde_json::from_str(&rumor.as_json()).unwrap();
625 json["id"] = serde_json::Value::String("00".repeat(32));
626 let forged_json = json.to_string();
627 let seal = EventBuilder::new(Kind::Custom(KIND_SEAL_PLAINTEXT), forged_json)
630 .custom_created_at(rumor.created_at)
631 .finalize(&author)
632 .unwrap();
633 let (wrap, _) = wrap_seal(&seal, &group(), KIND_WRAP, Timestamp::from_secs(1)).unwrap();
634 assert!(matches!(open_wrap(&wrap, &group()), Err(StreamError::BadRumorId)));
635 }
636
637 #[test]
638 fn ms_is_strict_absent_is_zero_invalid_is_dropped() {
639 let author = Keys::generate();
640 let rumor = build_rumor_secs(kind::MESSAGE, author.public_key(), "x", vec![], 1_000);
642 assert_eq!(resolve_ms_strict(&rumor).unwrap(), 1_000_000);
643 let ok = build_rumor_secs(
645 kind::MESSAGE,
646 author.public_key(),
647 "x",
648 vec![Tag::custom("ms", ["999".to_string()])],
649 1_000,
650 );
651 assert_eq!(resolve_ms_strict(&ok).unwrap(), 1_000_999);
652 for bad in ["1000", "-1", "12.5", "abc", "007", "", "+5", "+0", "+000", "+999"] {
656 let r = build_rumor_secs(
657 kind::MESSAGE,
658 author.public_key(),
659 "x",
660 vec![Tag::custom("ms", [bad.to_string()])],
661 1_000,
662 );
663 assert!(
664 matches!(resolve_ms_strict(&r), Err(StreamError::BadMs)),
665 "ms={bad:?} must be malformed"
666 );
667 }
668 }
669
670 #[test]
671 fn a_valueless_ms_tag_is_malformed_and_a_duplicate_takes_the_first() {
672 let author = Keys::generate();
675 let bare = build_rumor_secs(
676 kind::MESSAGE,
677 author.public_key(),
678 "x",
679 vec![Tag::custom("ms", Vec::<String>::new())],
680 1_000,
681 );
682 assert!(matches!(resolve_ms_strict(&bare), Err(StreamError::BadMs)));
683 let two = build_rumor_secs(
686 kind::MESSAGE,
687 author.public_key(),
688 "x",
689 vec![
690 Tag::custom("ms", Vec::<String>::new()),
691 Tag::custom("ms", ["5".to_string()]),
692 ],
693 1_000,
694 );
695 assert!(matches!(resolve_ms_strict(&two), Err(StreamError::BadMs)));
696 let two_valued = build_rumor_secs(
703 kind::MESSAGE,
704 author.public_key(),
705 "x",
706 vec![
707 Tag::custom("ms", ["1".to_string()]),
708 Tag::custom("ms", ["2".to_string()]),
709 ],
710 1_000,
711 );
712 assert_eq!(resolve_ms_strict(&two_valued).unwrap(), 1_000_001, "the FIRST ms wins");
713 }
714
715 #[test]
716 fn tag_numbers_are_spec_shaped_decimals() {
717 use crate::community::edition::is_tag_decimal;
718 for good in ["4", "0", "1099511627776"] {
720 assert!(is_tag_decimal(good), "{good:?} is decimal form");
721 }
722 for bad in ["04", "007", "00", "+4", "-4", "0x4", "1e2", " 4", "4 ", "", "4.0"] {
723 assert!(!is_tag_decimal(bad), "{bad:?} is NOT decimal form");
724 }
725 }
726
727 #[test]
728 fn a_citation_version_must_be_plain_digits() {
729 let eid = "ab".repeat(32);
734 let hash = "cd".repeat(32);
735 let cite = |v: &str| {
736 let tags = nostr_sdk::prelude::Tags::from_list(vec![Tag::custom(
737 "vac",
738 [eid.clone(), v.to_string(), hash.clone()],
739 )]);
740 crate::community::edition::AuthorityCitation::from_tags(&tags)
741 };
742 assert!(cite("5").is_some(), "a plain decimal is the shape the spec names");
743 assert!(cite("+5").is_none(), "a leading plus is not decimal form");
744 assert!(cite("").is_none());
745 assert!(cite("5x").is_none());
746 assert!(cite("05").is_none(), "no leading zeros (CORD-01 §5)");
747 }
748
749 #[test]
750 fn binding_rejects_splices_and_duplicates() {
751 let author = Keys::generate();
752 let tags = channel_binding_tags(&chan(), Epoch(0));
753 let rumor = build_rumor_ms(kind::MESSAGE, author.public_key(), "x", tags, 1_000);
754 assert!(matches!(
756 check_channel_binding(&rumor, &ChannelId([0xcd; 32]), Epoch(0)),
757 Err(StreamError::ChannelMismatch)
758 ));
759 assert!(matches!(
760 check_channel_binding(&rumor, &chan(), Epoch(1)),
761 Err(StreamError::EpochMismatch)
762 ));
763 let mut tags = channel_binding_tags(&chan(), Epoch(0));
765 tags.extend(channel_binding_tags(&chan(), Epoch(0)));
766 let dup = build_rumor_ms(kind::MESSAGE, author.public_key(), "x", tags, 1_000);
767 assert!(matches!(
768 check_channel_binding(&dup, &chan(), Epoch(0)),
769 Err(StreamError::DuplicateTag(_))
770 ));
771 let bare = build_rumor_ms(kind::MESSAGE, author.public_key(), "x", vec![], 1_000);
773 assert!(matches!(
774 check_channel_binding(&bare, &chan(), Epoch(0)),
775 Err(StreamError::MissingTag(_))
776 ));
777 }
778
779 #[test]
780 fn oversize_plaintext_is_refused_at_build_time() {
781 let author = Keys::generate();
782 let big = "x".repeat(NIP44_MAX_PLAINTEXT + 1);
783 let rumor = build_rumor_ms(kind::MESSAGE, author.public_key(), &big, vec![], 1_000);
784 assert!(matches!(
785 seal_content(&rumor, SealForm::Encrypted, &group()),
786 Err(StreamError::Oversize(_))
787 ));
788 }
789
790 #[test]
791 fn ephemeral_wrap_round_trips_and_bad_wrap_kind_rejects() {
792 let author = Keys::generate();
793 let tags = channel_binding_tags(&chan(), Epoch(0));
794 let rumor = build_rumor_ms(kind::TYPING, author.public_key(), "", tags, 5_000);
795 let seal = build_seal(&rumor, SealForm::Encrypted, &group(), &author).unwrap();
796 let (wrap, _) = wrap_seal(&seal, &group(), KIND_WRAP_EPHEMERAL, Timestamp::from_secs(5)).unwrap();
797 assert_eq!(wrap.kind.as_u16(), KIND_WRAP_EPHEMERAL);
798 assert_eq!(open_wrap(&wrap, &group()).unwrap().rumor.kind.as_u16(), kind::TYPING);
799 assert!(matches!(
800 wrap_seal(&seal, &group(), 1058, Timestamp::from_secs(5)),
801 Err(StreamError::BadWrapKind(1058))
802 ));
803 }
804
805 #[test]
806 fn rewrap_preserves_rumor_id_and_signature_across_epochs() {
807 let author = Keys::generate();
808 let wrap = send(&author, &group(), SealForm::Plaintext, "the head edition", 9_000);
809 let opened = open_wrap(&wrap, &group()).unwrap();
810
811 let next = channel_group_key(&[7u8; 32], &chan(), Epoch(1));
813 let (rewrapped, _) = rewrap_seal(&opened.seal, &next, Timestamp::from_secs(2_000)).unwrap();
814 let reopened = open_wrap(&rewrapped, &next).unwrap();
815
816 assert_eq!(reopened.rumor_id, opened.rumor_id, "rumor id survives the re-wrap");
817 assert_eq!(reopened.author, author.public_key(), "authorship survives");
818 assert_eq!(reopened.seal.sig, opened.seal.sig, "the original signature rides verbatim");
819 assert_ne!(reopened.wrapper_id, opened.wrapper_id, "outer identity differs per wrap");
820
821 let enc = send(&author, &group(), SealForm::Encrypted, "no", 9_000);
823 let enc_opened = open_wrap(&enc, &group()).unwrap();
824 assert!(matches!(
825 rewrap_seal(&enc_opened.seal, &next, Timestamp::from_secs(2_000)),
826 Err(StreamError::NotRewrappable)
827 ));
828 }
829
830 #[test]
831 fn wrap_p_tag_is_ephemeral_not_the_stream_or_author() {
832 let author = Keys::generate();
833 let g = group();
834 let tags = channel_binding_tags(&chan(), Epoch(0));
835 let rumor = build_rumor_ms(kind::MESSAGE, author.public_key(), "x", tags, 1_000);
836 let seal = build_seal(&rumor, SealForm::Encrypted, &g, &author).unwrap();
837 let (wrap, ephemeral) = wrap_seal(&seal, &g, KIND_WRAP, Timestamp::from_secs(1)).unwrap();
838 let p = wrap
839 .tags
840 .iter()
841 .find_map(|t| {
842 let s = t.as_slice();
843 (s.len() >= 2 && s[0] == "p").then(|| s[1].clone())
844 })
845 .expect("wrap carries a p tag");
846 assert_eq!(p, ephemeral.public_key().to_hex());
847 assert_ne!(p, g.pk_hex());
848 assert_ne!(p, author.public_key().to_hex());
849 }
850}