1#[cfg(feature = "full")]
2use crate::{AuthorizedDevice, DeviceRoster, GroupSenderKeyMessageEnvelope};
3use crate::{
4 DevicePubkey, Error as CoreError, Invite, InviteResponseEnvelope, MessageEnvelope, OwnerPubkey,
5 UnixSeconds,
6};
7#[cfg(feature = "full")]
8use base64::Engine;
9#[cfg(feature = "full")]
10use nostr::nips::nip44;
11use nostr::{Event, EventBuilder, Keys, Kind, Tag, Timestamp, UnsignedEvent};
12use thiserror::Error;
13
14pub const MESSAGE_EVENT_KIND: u32 = 1060;
15#[cfg(feature = "full")]
16pub const GROUP_SENDER_KEY_MESSAGE_KIND: u32 = MESSAGE_EVENT_KIND;
17pub const INVITE_EVENT_KIND: u32 = 30078;
18pub const INVITE_RESPONSE_KIND: u32 = 1059;
19#[cfg(feature = "full")]
20pub const ROSTER_EVENT_KIND: u32 = 30078;
21
22#[cfg(feature = "full")]
23const ROSTER_D_TAG: &str = "double-ratchet/app-keys";
24#[cfg(feature = "full")]
25const ROSTER_VERSION: &str = "1";
26pub const INVITE_LIST_LABEL: &str = "double-ratchet/invites";
27#[cfg(feature = "full")]
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct DecodedRosterEvent {
30 pub owner_pubkey: OwnerPubkey,
31 pub roster: DeviceRoster,
32}
33
34#[cfg(feature = "full")]
35#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
36pub struct ParsedGroupSenderKeyMessageEvent {
37 pub sender_event_pubkey: DevicePubkey,
38 pub key_id: u32,
39 pub message_number: u32,
40 #[serde(default, skip_serializing_if = "Option::is_none")]
41 pub encrypted_header: Option<String>,
42 pub created_at: UnixSeconds,
43 pub ciphertext: Vec<u8>,
44}
45
46#[derive(Error, Debug)]
47pub enum Error {
48 #[error(transparent)]
49 Core(#[from] CoreError),
50
51 #[error(transparent)]
52 Json(#[from] serde_json::Error),
53
54 #[error("invalid event: {0}")]
55 InvalidEvent(String),
56
57 #[error(transparent)]
58 NostrEvent(#[from] nostr::event::Error),
59
60 #[error(transparent)]
61 NostrKey(#[from] nostr::key::Error),
62
63 #[error(transparent)]
64 Nip44(#[from] nostr::nips::nip44::Error),
65}
66
67pub type Result<T> = std::result::Result<T, Error>;
68
69impl From<Error> for CoreError {
70 fn from(error: Error) -> Self {
71 CoreError::Parse(error.to_string())
72 }
73}
74
75pub fn message_event(envelope: &MessageEnvelope) -> Result<Event> {
76 require_privacy_version(envelope.metadata_privacy_version)?;
77 let author_secret_key = secret_key_from_bytes(&envelope.signer_secret_key)?;
78 let author_keys = Keys::new(author_secret_key);
79 let derived_sender = DevicePubkey::from_bytes(author_keys.public_key().to_bytes());
80 if derived_sender != envelope.sender {
81 return Err(Error::InvalidEvent(
82 "sender does not match signer secret".to_string(),
83 ));
84 }
85
86 if envelope.recipient.is_some() {
89 return Err(Error::InvalidEvent(
90 "message events must not reveal a recipient key".to_string(),
91 ));
92 }
93
94 let unsigned = EventBuilder::new(
95 Kind::from(MESSAGE_EVENT_KIND as u16),
96 envelope.ciphertext.clone(),
97 )
98 .tag(tag(["v", "2"])?)
99 .tag(tag(["header", envelope.encrypted_header.as_str()])?)
100 .custom_created_at(Timestamp::from(envelope.created_at.get()))
101 .build(public_key(envelope.sender)?);
102
103 Ok(unsigned.sign_with_keys(&author_keys)?)
104}
105
106pub fn parse_message_event(event: &Event) -> Result<MessageEnvelope> {
107 require_private_event(event)?;
108 verify_event_kind(event, MESSAGE_EVENT_KIND)?;
109 event.verify()?;
110 let encrypted_header = required_tag_value(event, "header")?;
111 if encrypted_header.is_empty() {
112 return Err(Error::InvalidEvent("empty `header` tag".to_string()));
113 }
114
115 Ok(MessageEnvelope {
116 metadata_privacy_version: crate::METADATA_PRIVACY_VERSION,
117 sender: DevicePubkey::from_bytes(event.pubkey.to_bytes()),
118 recipient: optional_tag_value(event, "p")
119 .as_deref()
120 .map(parse_device_pubkey)
121 .transpose()?,
122 signer_secret_key: [0u8; 32],
123 created_at: UnixSeconds(event.created_at.as_secs()),
124 encrypted_header,
125 ciphertext: event.content.clone(),
126 })
127}
128
129#[cfg(feature = "full")]
130pub fn group_sender_key_message_event(envelope: &GroupSenderKeyMessageEnvelope) -> Result<Event> {
131 let author_secret_key = secret_key_from_bytes(&envelope.signer_secret_key)?;
132 let author_keys = Keys::new(author_secret_key);
133 let derived_sender = DevicePubkey::from_bytes(author_keys.public_key().to_bytes());
134 if derived_sender != envelope.sender_event_pubkey {
135 return Err(Error::InvalidEvent(
136 "sender-event pubkey does not match signer secret".to_string(),
137 ));
138 }
139
140 let content = build_group_sender_key_hidden_content(&envelope.ciphertext);
141 let encrypted_header = match envelope.encrypted_header.clone() {
142 Some(encrypted_header) => encrypted_header,
143 None => encrypted_cover_header(&author_keys)?,
144 };
145 let unsigned = EventBuilder::new(Kind::from(GROUP_SENDER_KEY_MESSAGE_KIND as u16), content)
146 .tag(tag(["v", "2"])?)
147 .tag(tag(["header", encrypted_header.as_str()])?)
148 .custom_created_at(Timestamp::from(envelope.created_at.get()))
149 .build(public_key(envelope.sender_event_pubkey)?);
150
151 Ok(unsigned.sign_with_keys(&author_keys)?)
152}
153
154#[cfg(feature = "full")]
155pub fn parse_group_sender_key_message_event(
156 event: &Event,
157) -> Result<ParsedGroupSenderKeyMessageEvent> {
158 require_private_event(event)?;
159 verify_event_kind(event, GROUP_SENDER_KEY_MESSAGE_KIND)?;
160 event.verify()?;
161 if optional_tag_value(event, "header").is_some() {
162 return Err(Error::InvalidEvent(
163 "camouflaged group sender-key event requires caller-side sender match".to_string(),
164 ));
165 }
166
167 parsed_group_sender_key_message_event_from_content(event)
168}
169
170#[cfg(feature = "full")]
171pub fn parse_group_sender_key_message_event_unchecked(
172 event: &Event,
173) -> Result<ParsedGroupSenderKeyMessageEvent> {
174 require_private_event(event)?;
175 verify_event_kind(event, GROUP_SENDER_KEY_MESSAGE_KIND)?;
176 event.verify()?;
177 parsed_group_sender_key_message_event_from_content(event)
178}
179
180#[cfg(feature = "full")]
181fn parsed_group_sender_key_message_event_from_content(
182 event: &Event,
183) -> Result<ParsedGroupSenderKeyMessageEvent> {
184 let encrypted_header = optional_tag_value(event, "header");
185 let parsed = if encrypted_header.is_some() {
186 (0, 0, parse_group_sender_key_hidden_content(&event.content)?)
187 } else {
188 parse_group_sender_key_legacy_content(&event.content)?
189 };
190
191 Ok(ParsedGroupSenderKeyMessageEvent {
192 sender_event_pubkey: DevicePubkey::from_bytes(event.pubkey.to_bytes()),
193 key_id: parsed.0,
194 message_number: parsed.1,
195 encrypted_header,
196 created_at: UnixSeconds(event.created_at.as_secs()),
197 ciphertext: parsed.2,
198 })
199}
200
201#[cfg(feature = "full")]
202pub(crate) fn encrypted_cover_header_tag(keys: &Keys) -> Result<Tag> {
203 let encrypted = encrypted_cover_header(keys)?;
204 tag(["header", encrypted.as_str()])
205}
206
207#[cfg(feature = "full")]
208fn encrypted_cover_header(keys: &Keys) -> Result<String> {
209 let cover = serde_json::json!({
210 "v": 1,
211 "type": "sender-key-cover",
212 });
213 Ok(nip44::encrypt(
214 keys.secret_key(),
215 &keys.public_key(),
216 serde_json::to_string(&cover)?,
217 nip44::Version::V2,
218 )?)
219}
220
221#[cfg(feature = "full")]
222fn build_group_sender_key_hidden_content(ciphertext: &[u8]) -> String {
223 base64::engine::general_purpose::STANDARD.encode(ciphertext)
224}
225
226#[cfg(feature = "full")]
227fn parse_group_sender_key_hidden_content(content: &str) -> Result<Vec<u8>> {
228 base64::engine::general_purpose::STANDARD
229 .decode(content)
230 .map_err(|e| Error::InvalidEvent(e.to_string()))
231}
232
233#[cfg(all(test, feature = "full"))]
234fn build_group_sender_key_legacy_content(
235 key_id: u32,
236 message_number: u32,
237 ciphertext: &[u8],
238) -> String {
239 let mut payload = Vec::with_capacity(8 + ciphertext.len());
240 payload.extend_from_slice(&key_id.to_be_bytes());
241 payload.extend_from_slice(&message_number.to_be_bytes());
242 payload.extend_from_slice(ciphertext);
243 base64::engine::general_purpose::STANDARD.encode(payload)
244}
245
246#[cfg(feature = "full")]
247fn parse_group_sender_key_legacy_content(content: &str) -> Result<(u32, u32, Vec<u8>)> {
248 let bytes = base64::engine::general_purpose::STANDARD
249 .decode(content)
250 .map_err(|e| Error::InvalidEvent(e.to_string()))?;
251 if bytes.len() < 8 {
252 return Err(Error::InvalidEvent(
253 "group sender-key payload too short".to_string(),
254 ));
255 }
256 let key_id = u32::from_be_bytes(
257 bytes[0..4]
258 .try_into()
259 .map_err(|_| Error::InvalidEvent("invalid key_id bytes".to_string()))?,
260 );
261 let message_number = u32::from_be_bytes(
262 bytes[4..8]
263 .try_into()
264 .map_err(|_| Error::InvalidEvent("invalid message number bytes".to_string()))?,
265 );
266 Ok((key_id, message_number, bytes[8..].to_vec()))
267}
268
269pub fn invite_url(invite: &Invite, root: &str) -> Result<String> {
270 let mut data = serde_json::Map::new();
271 data.insert(
272 "version".into(),
273 serde_json::json!(crate::METADATA_PRIVACY_VERSION),
274 );
275 data.insert(
276 "inviter".to_string(),
277 serde_json::Value::String(invite.inviter_device_pubkey.to_string()),
278 );
279 data.insert(
280 "ephemeralKey".to_string(),
281 serde_json::Value::String(invite.inviter_ephemeral_public_key.to_string()),
282 );
283 data.insert(
284 "sharedSecret".to_string(),
285 serde_json::Value::String(hex::encode(invite.shared_secret)),
286 );
287 data.insert(
288 "createdAt".to_string(),
289 serde_json::Value::Number(serde_json::Number::from(invite.created_at.get())),
290 );
291 if let Some(device_id) = invite.device_id.as_ref() {
292 data.insert(
293 "deviceId".to_string(),
294 serde_json::Value::String(device_id.clone()),
295 );
296 }
297 let owner = invite
298 .owner_public_key
299 .map(|pk| OwnerPubkey::from_bytes(pk.to_bytes()))
300 .or(invite.inviter_owner_pubkey);
301 if let Some(owner) = owner {
302 data.insert(
303 "owner".to_string(),
304 serde_json::Value::String(owner.to_string()),
305 );
306 }
307 if let Some(purpose) = invite.purpose.as_ref() {
308 data.insert(
309 "purpose".to_string(),
310 serde_json::Value::String(purpose.clone()),
311 );
312 }
313 if let Some(max_uses) = invite.max_uses {
314 data.insert(
315 "maxUses".to_string(),
316 serde_json::Value::Number(serde_json::Number::from(max_uses)),
317 );
318 }
319
320 Ok(format!(
321 "{root}#{}",
322 urlencoding::encode(&serde_json::Value::Object(data).to_string())
323 ))
324}
325
326pub fn parse_invite_url(url: &str) -> Result<Invite> {
327 let hash = url
328 .split('#')
329 .nth(1)
330 .ok_or_else(|| Error::InvalidEvent("no hash in invite URL".to_string()))?;
331 let decoded = urlencoding::decode(hash).map_err(|e| Error::InvalidEvent(e.to_string()))?;
332 let data: serde_json::Value = serde_json::from_str(&decoded)?;
333 require_privacy_version(
334 data["version"]
335 .as_u64()
336 .and_then(|v| u8::try_from(v).ok())
337 .unwrap_or(0),
338 )?;
339
340 let inviter_device_pubkey = parse_device_pubkey(
341 data.get("inviter")
342 .and_then(|value| value.as_str())
343 .ok_or_else(|| Error::InvalidEvent("missing inviter".to_string()))?,
344 )?;
345 let inviter_ephemeral_public_key = parse_device_pubkey(
346 data["ephemeralKey"]
347 .as_str()
348 .or_else(|| data["inviterEphemeralPublicKey"].as_str())
349 .ok_or_else(|| Error::InvalidEvent("missing ephemeralKey".to_string()))?,
350 )?;
351 let shared_secret = parse_hex_32(
352 data["sharedSecret"]
353 .as_str()
354 .ok_or_else(|| Error::InvalidEvent("missing sharedSecret".to_string()))?,
355 )?;
356 let inviter_owner_pubkey = data["owner"]
357 .as_str()
358 .or_else(|| data["ownerPubkey"].as_str())
359 .or_else(|| data["ownerPublicKey"].as_str())
360 .map(parse_owner_pubkey)
361 .transpose()?;
362
363 Ok(Invite {
364 metadata_privacy_version: crate::METADATA_PRIVACY_VERSION,
365 inviter_device_pubkey,
366 inviter_ephemeral_public_key,
367 shared_secret,
368 inviter_ephemeral_private_key: None,
369 max_uses: data["maxUses"].as_u64().map(|value| value as usize),
370 used_by: Vec::new(),
371 used_response_contents: Vec::new(),
372 created_at: UnixSeconds(data["createdAt"].as_u64().unwrap_or(0)),
373 inviter_owner_pubkey,
374 purpose: data["purpose"].as_str().map(ToString::to_string),
375 inviter: public_key(inviter_device_pubkey)?,
376 device_id: data["deviceId"].as_str().map(ToString::to_string),
377 owner_public_key: inviter_owner_pubkey
378 .map(|owner| public_key(DevicePubkey::from_bytes(owner.to_bytes())))
379 .transpose()?,
380 })
381}
382
383pub fn invite_unsigned_event(invite: &Invite) -> Result<UnsignedEvent> {
384 let inviter_device_pubkey = invite.inviter_device_pubkey;
385 let d_suffix = invite
386 .device_id
387 .clone()
388 .unwrap_or_else(|| inviter_device_pubkey.to_string());
389 let mut builder = EventBuilder::new(Kind::from(INVITE_EVENT_KIND as u16), "")
390 .tag(tag(["v", "2"])?)
391 .tag(tag([
392 "ephemeralKey",
393 &invite.inviter_ephemeral_public_key.to_string(),
394 ])?)
395 .tag(tag(["sharedSecret", &hex::encode(invite.shared_secret)])?)
396 .tag(tag(["d", &format!("double-ratchet/invites/{d_suffix}")])?)
397 .tag(tag(["l", INVITE_LIST_LABEL])?)
398 .custom_created_at(Timestamp::from(invite.created_at.get()));
399
400 let owner = invite
401 .owner_public_key
402 .map(|pk| OwnerPubkey::from_bytes(pk.to_bytes()))
403 .or(invite.inviter_owner_pubkey);
404 if let Some(inviter_owner_pubkey) = owner {
405 builder = builder.tag(tag(["ownerPublicKey", &inviter_owner_pubkey.to_string()])?);
406 }
407
408 Ok(builder.build(public_key(inviter_device_pubkey)?))
409}
410
411pub fn parse_invite_event(event: &Event) -> Result<Invite> {
412 require_private_event(event)?;
413 verify_event_kind(event, INVITE_EVENT_KIND)?;
414 event.verify()?;
415
416 let device_id = parse_invite_d_tag(&required_tag_value(event, "d")?)?;
417 let inviter_device_pubkey = parse_device_pubkey(&device_id)
418 .unwrap_or_else(|_| DevicePubkey::from_bytes(event.pubkey.to_bytes()));
419 let inviter_owner_pubkey = optional_tag_value(event, "ownerPublicKey")
420 .map(|value| parse_owner_pubkey(&value))
421 .transpose()?;
422 if event.pubkey.to_bytes() != inviter_device_pubkey.to_bytes() {
423 return Err(Error::InvalidEvent(
424 "invite event author does not match inviter device".to_string(),
425 ));
426 }
427
428 Ok(Invite {
429 metadata_privacy_version: crate::METADATA_PRIVACY_VERSION,
430 inviter_device_pubkey,
431 inviter_ephemeral_public_key: parse_device_pubkey(&required_tag_value(
432 event,
433 "ephemeralKey",
434 )?)?,
435 shared_secret: parse_hex_32(&required_tag_value(event, "sharedSecret")?)?,
436 inviter_ephemeral_private_key: None,
437 max_uses: None,
438 used_by: Vec::new(),
439 used_response_contents: Vec::new(),
440 created_at: UnixSeconds(event.created_at.as_secs()),
441 inviter_owner_pubkey,
442 purpose: None,
443 inviter: event.pubkey,
444 device_id: (device_id != inviter_device_pubkey.to_string()).then_some(device_id),
445 owner_public_key: inviter_owner_pubkey
446 .map(|owner| public_key(DevicePubkey::from_bytes(owner.to_bytes())))
447 .transpose()?,
448 })
449}
450
451pub fn invite_response_event(envelope: &InviteResponseEnvelope) -> Result<Event> {
452 require_privacy_version(envelope.metadata_privacy_version)?;
453 let author_secret_key = secret_key_from_bytes(&envelope.signer_secret_key)?;
454 let author_keys = Keys::new(author_secret_key);
455 let derived_sender = DevicePubkey::from_bytes(author_keys.public_key().to_bytes());
456 if derived_sender != envelope.sender {
457 return Err(Error::InvalidEvent(
458 "sender does not match signer secret".to_string(),
459 ));
460 }
461
462 let unsigned = EventBuilder::new(
463 Kind::from(INVITE_RESPONSE_KIND as u16),
464 envelope.content.clone(),
465 )
466 .tag(tag(["v", "2"])?)
467 .tag(tag(["p", &envelope.recipient.to_string()])?)
468 .custom_created_at(Timestamp::from(envelope.created_at.get()))
469 .build(public_key(envelope.sender)?);
470
471 Ok(unsigned.sign_with_keys(&author_keys)?)
472}
473
474pub fn parse_invite_response_event(event: &Event) -> Result<InviteResponseEnvelope> {
475 require_private_event(event)?;
476 verify_event_kind(event, INVITE_RESPONSE_KIND)?;
477 event.verify()?;
478 let recipient = parse_device_pubkey(&required_tag_value(event, "p")?)?;
479
480 Ok(InviteResponseEnvelope {
481 metadata_privacy_version: crate::METADATA_PRIVACY_VERSION,
482 sender: DevicePubkey::from_bytes(event.pubkey.to_bytes()),
483 signer_secret_key: [0u8; 32],
484 recipient,
485 created_at: UnixSeconds(event.created_at.as_secs()),
486 content: event.content.clone(),
487 })
488}
489
490#[cfg(feature = "full")]
491pub fn roster_unsigned_event(
492 owner_pubkey: OwnerPubkey,
493 roster: &DeviceRoster,
494) -> Result<UnsignedEvent> {
495 let mut builder = EventBuilder::new(Kind::from(ROSTER_EVENT_KIND as u16), "")
496 .tag(tag(["d", ROSTER_D_TAG])?)
497 .tag(tag(["version", ROSTER_VERSION])?)
498 .custom_created_at(Timestamp::from(roster.created_at.get()));
499
500 for device in roster.devices() {
501 builder = builder.tag(tag([
502 "device",
503 &device.device_pubkey.to_string(),
504 &device.created_at.get().to_string(),
505 ])?);
506 }
507
508 Ok(builder.build(owner_public_key(owner_pubkey)?))
509}
510
511#[cfg(feature = "full")]
512pub fn parse_roster_event(event: &Event) -> Result<DecodedRosterEvent> {
513 verify_event_kind(event, ROSTER_EVENT_KIND)?;
514 event.verify()?;
515 if optional_tag_value(event, "d").as_deref() != Some(ROSTER_D_TAG) {
516 return Err(Error::InvalidEvent("missing roster d tag".to_string()));
517 }
518
519 let mut devices = Vec::new();
520 for tag in event.tags.iter() {
521 let values = tag.as_slice();
522 if values.first().map(|value| value.as_str()) != Some("device") {
523 continue;
524 }
525 let device_pubkey = parse_device_pubkey(
526 values
527 .get(1)
528 .ok_or_else(|| Error::InvalidEvent("device tag missing pubkey".to_string()))?,
529 )?;
530 let created_at = values
531 .get(2)
532 .ok_or_else(|| Error::InvalidEvent("device tag missing created_at".to_string()))?
533 .parse::<u64>()
534 .map_err(|e| Error::InvalidEvent(e.to_string()))?;
535 devices.push(AuthorizedDevice {
536 device_pubkey,
537 created_at: UnixSeconds(created_at),
538 });
539 }
540
541 Ok(DecodedRosterEvent {
542 owner_pubkey: OwnerPubkey::from_bytes(event.pubkey.to_bytes()),
543 roster: DeviceRoster::new(UnixSeconds(event.created_at.as_secs()), devices),
544 })
545}
546
547fn verify_event_kind(event: &Event, expected_kind: u32) -> Result<()> {
548 if event.kind != Kind::from(expected_kind as u16) {
549 return Err(Error::InvalidEvent(format!(
550 "unexpected kind {}, expected {}",
551 event.kind, expected_kind
552 )));
553 }
554 Ok(())
555}
556
557fn tag<const N: usize>(parts: [&str; N]) -> Result<Tag> {
558 Tag::parse(parts.map(str::to_owned)).map_err(|e| Error::InvalidEvent(e.to_string()))
559}
560
561fn required_tag_value(event: &Event, key: &str) -> Result<String> {
562 optional_tag_value(event, key)
563 .ok_or_else(|| Error::InvalidEvent(format!("missing `{key}` tag")))
564}
565
566fn optional_tag_value(event: &Event, key: &str) -> Option<String> {
567 event
568 .tags
569 .iter()
570 .find(|tag| tag.as_slice().first().map(|value| value.as_str()) == Some(key))
571 .and_then(|tag| tag.as_slice().get(1).map(ToOwned::to_owned))
572}
573
574fn parse_owner_pubkey(value: &str) -> Result<OwnerPubkey> {
575 let pubkey = nostr::PublicKey::parse(value)?;
576 Ok(OwnerPubkey::from_bytes(pubkey.to_bytes()))
577}
578
579fn parse_device_pubkey(value: &str) -> Result<DevicePubkey> {
580 let pubkey = nostr::PublicKey::parse(value)?;
581 Ok(DevicePubkey::from_bytes(pubkey.to_bytes()))
582}
583
584fn parse_invite_d_tag(value: &str) -> Result<String> {
585 value
586 .strip_prefix("double-ratchet/invites/")
587 .map(ToString::to_string)
588 .ok_or_else(|| Error::InvalidEvent("invalid invite d tag".to_string()))
589}
590
591fn parse_hex_32(value: &str) -> Result<[u8; 32]> {
592 let bytes = hex::decode(value).map_err(|e| Error::InvalidEvent(e.to_string()))?;
593 <[u8; 32]>::try_from(bytes.as_slice())
594 .map_err(|_| Error::InvalidEvent("expected 32-byte hex".to_string()))
595}
596
597fn secret_key_from_bytes(bytes: &[u8; 32]) -> Result<nostr::SecretKey> {
598 Ok(nostr::SecretKey::from_slice(bytes)?)
599}
600
601fn public_key(device_pubkey: DevicePubkey) -> Result<nostr::PublicKey> {
602 Ok(nostr::PublicKey::from_slice(&device_pubkey.to_bytes())?)
603}
604
605#[cfg(feature = "full")]
606fn owner_public_key(owner_pubkey: OwnerPubkey) -> Result<nostr::PublicKey> {
607 Ok(nostr::PublicKey::from_slice(&owner_pubkey.to_bytes())?)
608}
609
610#[cfg(test)]
611mod tests {
612 use super::*;
613
614 #[test]
615 fn message_event_roundtrip() {
616 let signer_secret = [21u8; 32];
617 let sender = DevicePubkey::from_bytes(
618 Keys::new(secret_key_from_bytes(&signer_secret).unwrap())
619 .public_key()
620 .to_bytes(),
621 );
622 let event = message_event(&MessageEnvelope {
623 metadata_privacy_version: crate::METADATA_PRIVACY_VERSION,
624 sender,
625 recipient: None,
626 signer_secret_key: signer_secret,
627 created_at: UnixSeconds(10),
628 encrypted_header: "header".to_string(),
629 ciphertext: "ciphertext".to_string(),
630 })
631 .unwrap();
632 assert!(optional_tag_value(&event, "p").is_none());
633
634 let parsed = parse_message_event(&event).unwrap();
635 assert_eq!(parsed.sender, sender);
636 assert_eq!(parsed.recipient, None);
637 assert_eq!(parsed.encrypted_header, "header");
638 assert_eq!(parsed.ciphertext, "ciphertext");
639 }
640
641 #[cfg(feature = "full")]
642 #[test]
643 fn group_sender_key_message_event_roundtrip_is_camouflaged_as_message_event() {
644 let signer_secret = [22u8; 32];
645 let sender_event_pubkey = DevicePubkey::from_bytes(
646 Keys::new(secret_key_from_bytes(&signer_secret).unwrap())
647 .public_key()
648 .to_bytes(),
649 );
650 let event = group_sender_key_message_event(&GroupSenderKeyMessageEnvelope {
651 group_id: "group-1".to_string(),
652 sender_event_pubkey,
653 signer_secret_key: signer_secret,
654 key_id: 7,
655 message_number: 11,
656 encrypted_header: None,
657 created_at: UnixSeconds(12),
658 ciphertext: b"ciphertext".to_vec(),
659 })
660 .unwrap();
661
662 assert_eq!(event.kind.as_u16() as u32, MESSAGE_EVENT_KIND);
663 assert_eq!(GROUP_SENDER_KEY_MESSAGE_KIND, MESSAGE_EVENT_KIND);
664 assert!(optional_tag_value(&event, "header").is_some());
665 assert_eq!(
666 base64::engine::general_purpose::STANDARD
667 .decode(&event.content)
668 .unwrap(),
669 b"ciphertext"
670 );
671 assert!(parse_group_sender_key_message_event(&event).is_err());
672 let parsed = parse_group_sender_key_message_event_unchecked(&event).unwrap();
673 assert_eq!(parsed.sender_event_pubkey, sender_event_pubkey);
674 assert_eq!(parsed.key_id, 0);
675 assert_eq!(parsed.message_number, 0);
676 assert!(parsed.encrypted_header.is_some());
677 assert_eq!(parsed.ciphertext, b"ciphertext");
678 }
679
680 #[cfg(feature = "full")]
681 #[test]
682 fn unversioned_group_sender_key_message_is_rejected() {
683 let signer_secret = [23u8; 32];
684 let signer_keys = Keys::new(secret_key_from_bytes(&signer_secret).unwrap());
685 let content = build_group_sender_key_legacy_content(8, 12, b"legacy ciphertext");
686 let event = EventBuilder::new(Kind::from(GROUP_SENDER_KEY_MESSAGE_KIND as u16), content)
687 .custom_created_at(Timestamp::from(13))
688 .build(signer_keys.public_key())
689 .sign_with_keys(&signer_keys)
690 .unwrap();
691
692 assert!(optional_tag_value(&event, "header").is_none());
693 assert!(parse_group_sender_key_message_event(&event).is_err());
694 }
695
696 #[test]
697 fn invite_url_and_event_roundtrip() {
698 let owner_secret = [31u8; 32];
699 let owner_pubkey = OwnerPubkey::from_bytes(
700 Keys::new(secret_key_from_bytes(&owner_secret).unwrap())
701 .public_key()
702 .to_bytes(),
703 );
704 let signer_secret = [32u8; 32];
705 let inviter_device_pubkey = DevicePubkey::from_bytes(
706 Keys::new(secret_key_from_bytes(&signer_secret).unwrap())
707 .public_key()
708 .to_bytes(),
709 );
710 let invite = Invite {
711 metadata_privacy_version: crate::METADATA_PRIVACY_VERSION,
712 inviter_device_pubkey,
713 inviter_ephemeral_public_key: DevicePubkey::from_bytes([9u8; 32]),
714 shared_secret: [7u8; 32],
715 inviter_ephemeral_private_key: Some([8u8; 32]),
716 max_uses: None,
717 used_by: Vec::new(),
718 used_response_contents: Vec::new(),
719 created_at: UnixSeconds(22),
720 inviter_owner_pubkey: Some(owner_pubkey),
721 purpose: None,
722 inviter: inviter_device_pubkey.to_nostr().unwrap(),
723 device_id: None,
724 owner_public_key: Some(owner_pubkey.to_nostr().unwrap()),
725 };
726
727 let url = invite_url(&invite, "https://chat.iris.to").unwrap();
728 let encoded = url.split('#').nth(1).unwrap();
729 let decoded = urlencoding::decode(encoded).unwrap();
730 let data: serde_json::Value = serde_json::from_str(&decoded).unwrap();
731 assert_eq!(
732 data["inviter"].as_str().unwrap(),
733 invite.inviter_device_pubkey.to_string()
734 );
735 assert!(data.get("inviterDevice").is_none());
736
737 let parsed_from_url = parse_invite_url(&url).unwrap();
738 assert_eq!(
739 parsed_from_url.inviter_device_pubkey,
740 invite.inviter_device_pubkey
741 );
742 assert_eq!(
743 parsed_from_url.inviter_owner_pubkey,
744 invite.inviter_owner_pubkey
745 );
746 assert_eq!(parsed_from_url.created_at, invite.created_at);
747
748 let unsigned = invite_unsigned_event(&invite).unwrap();
749 let keys = Keys::new(secret_key_from_bytes(&signer_secret).unwrap());
750 assert_eq!(
751 keys.public_key().to_bytes(),
752 inviter_device_pubkey.to_bytes()
753 );
754 let signed = unsigned.sign_with_keys(&keys).unwrap();
755 let parsed_from_event = parse_invite_event(&signed).unwrap();
756 assert_eq!(
757 parsed_from_event.inviter_device_pubkey,
758 invite.inviter_device_pubkey
759 );
760 assert_eq!(
761 parsed_from_event.inviter_owner_pubkey,
762 invite.inviter_owner_pubkey
763 );
764 }
765
766 #[test]
767 fn invite_url_preserves_private_one_use_metadata() {
768 let device = Keys::generate().public_key();
769 let owner = Keys::generate().public_key();
770 let mut invite =
771 Invite::create_new(device, Some(device.to_hex()), Some(1)).expect("invite");
772 invite.owner_public_key = Some(owner);
773 invite.inviter_owner_pubkey = Some(OwnerPubkey::from_bytes(owner.to_bytes()));
774 invite.purpose = Some("private".to_string());
775
776 let url = invite_url(&invite, "https://chat.iris.to").unwrap();
777 let parsed = parse_invite_url(&url).unwrap();
778
779 assert_eq!(parsed.device_id, Some(device.to_hex()));
780 assert_eq!(parsed.max_uses, Some(1));
781 assert_eq!(parsed.created_at, invite.created_at);
782 assert_eq!(parsed.owner_public_key, Some(owner));
783 assert_eq!(parsed.purpose.as_deref(), Some("private"));
784 }
785
786 #[test]
787 fn invite_url_accepts_owner_public_key_alias() {
788 let device = Keys::generate().public_key();
789 let owner = Keys::generate().public_key();
790 let payload = serde_json::json!({
791 "version": 2,
792 "inviter": device.to_hex(),
793 "ephemeralKey": device.to_hex(),
794 "sharedSecret": "01".repeat(32),
795 "ownerPublicKey": owner.to_hex(),
796 })
797 .to_string();
798 let encoded = urlencoding::encode(&payload);
799 let parsed = parse_invite_url(&format!("https://chat.iris.to/#{encoded}")).unwrap();
800
801 assert_eq!(parsed.owner_public_key, Some(owner));
802 assert_eq!(
803 parsed.inviter_owner_pubkey,
804 Some(OwnerPubkey::from_bytes(owner.to_bytes()))
805 );
806 }
807
808 #[test]
809 fn invite_url_rejects_inviter_device_wire_name() {
810 let data = serde_json::json!({
811 "inviterDevice": DevicePubkey::from_bytes([3u8; 32]).to_string(),
812 "ephemeralKey": DevicePubkey::from_bytes([9u8; 32]).to_string(),
813 "sharedSecret": hex::encode([7u8; 32]),
814 "createdAt": 22,
815 });
816 let url = format!(
817 "https://chat.iris.to#{}",
818 urlencoding::encode(&data.to_string())
819 );
820
821 assert!(parse_invite_url(&url).is_err());
822 }
823
824 #[test]
825 fn invite_response_event_roundtrip() {
826 let sender_secret = [22u8; 32];
827 let sender = DevicePubkey::from_bytes(
828 Keys::new(secret_key_from_bytes(&sender_secret).unwrap())
829 .public_key()
830 .to_bytes(),
831 );
832 let recipient = DevicePubkey::from_bytes([7u8; 32]);
833
834 let event = invite_response_event(&InviteResponseEnvelope {
835 metadata_privacy_version: crate::METADATA_PRIVACY_VERSION,
836 sender,
837 signer_secret_key: sender_secret,
838 recipient,
839 created_at: UnixSeconds(25),
840 content: "payload".to_string(),
841 })
842 .unwrap();
843
844 let parsed = parse_invite_response_event(&event).unwrap();
845 assert_eq!(parsed.sender, sender);
846 assert_eq!(parsed.recipient, recipient);
847 assert_eq!(parsed.created_at, UnixSeconds(25));
848 assert_eq!(parsed.content, "payload");
849 }
850
851 #[test]
852 fn invite_event_requires_device_author() {
853 let owner_secret = [31u8; 32];
854 let owner_pubkey = OwnerPubkey::from_bytes(
855 Keys::new(secret_key_from_bytes(&owner_secret).unwrap())
856 .public_key()
857 .to_bytes(),
858 );
859 let device_secret = [32u8; 32];
860 let inviter_device_pubkey = DevicePubkey::from_bytes(
861 Keys::new(secret_key_from_bytes(&device_secret).unwrap())
862 .public_key()
863 .to_bytes(),
864 );
865 let invite = Invite {
866 metadata_privacy_version: crate::METADATA_PRIVACY_VERSION,
867 inviter_device_pubkey,
868 inviter_ephemeral_public_key: DevicePubkey::from_bytes([9u8; 32]),
869 shared_secret: [7u8; 32],
870 inviter_ephemeral_private_key: Some([8u8; 32]),
871 max_uses: None,
872 used_by: Vec::new(),
873 used_response_contents: Vec::new(),
874 created_at: UnixSeconds(22),
875 inviter_owner_pubkey: Some(owner_pubkey),
876 purpose: None,
877 inviter: inviter_device_pubkey.to_nostr().unwrap(),
878 device_id: None,
879 owner_public_key: Some(owner_pubkey.to_nostr().unwrap()),
880 };
881
882 let unsigned = invite_unsigned_event(&invite).unwrap();
883 let owner_keys = Keys::new(secret_key_from_bytes(&owner_secret).unwrap());
884 let signed = unsigned.sign_with_keys(&owner_keys).unwrap();
885 assert!(parse_invite_event(&signed).is_err());
886 }
887
888 #[cfg(feature = "full")]
889 #[test]
890 fn roster_event_roundtrip() {
891 let owner_secret = [41u8; 32];
892 let owner = OwnerPubkey::from_bytes(
893 Keys::new(secret_key_from_bytes(&owner_secret).unwrap())
894 .public_key()
895 .to_bytes(),
896 );
897 let device_secret = [42u8; 32];
898 let device_pubkey = DevicePubkey::from_bytes(
899 Keys::new(secret_key_from_bytes(&device_secret).unwrap())
900 .public_key()
901 .to_bytes(),
902 );
903 let roster = DeviceRoster::new(
904 UnixSeconds(300),
905 vec![AuthorizedDevice::new(device_pubkey, UnixSeconds(100))],
906 );
907
908 let unsigned = roster_unsigned_event(owner, &roster).unwrap();
909 assert!(unsigned.tags.iter().any(|tag| {
910 let values = tag.as_slice();
911 values.first().map(|value| value.as_str()) == Some("d")
912 && values.get(1).map(|value| value.as_str()) == Some("double-ratchet/app-keys")
913 }));
914 let keys = Keys::new(secret_key_from_bytes(&owner_secret).unwrap());
915 let signed = unsigned.sign_with_keys(&keys).unwrap();
916
917 let decoded = parse_roster_event(&signed).unwrap();
918 assert_eq!(decoded.owner_pubkey, owner);
919 assert_eq!(decoded.roster, roster);
920
921 let legacy_roster_unsigned = EventBuilder::new(Kind::from(ROSTER_EVENT_KIND as u16), "")
922 .tag(tag(["d", "double-ratchet/roster"]).unwrap())
923 .tag(tag(["version", ROSTER_VERSION]).unwrap())
924 .tag(tag(["device", &device_pubkey.to_string(), "100"]).unwrap())
925 .custom_created_at(Timestamp::from(roster.created_at.get()))
926 .build(owner_public_key(owner).unwrap());
927 let legacy_roster_signed = legacy_roster_unsigned.sign_with_keys(&keys).unwrap();
928 assert!(parse_roster_event(&legacy_roster_signed).is_err());
929 }
930}
931
932fn require_privacy_version(version: u8) -> Result<()> {
933 if version != crate::METADATA_PRIVACY_VERSION {
934 return Err(Error::InvalidEvent(
935 "protocol requires a new private handshake".into(),
936 ));
937 }
938 Ok(())
939}
940
941fn require_private_event(event: &Event) -> Result<()> {
942 if !crate::has_metadata_privacy_version(event) {
943 return Err(Error::InvalidEvent(
944 "event requires a privacy upgrade".into(),
945 ));
946 }
947 Ok(())
948}