Skip to main content

nostr_double_ratchet/
wire.rs

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    // The recipient already knows the sender's ratchet key and subscribes by
87    // author. Naming it here would let relays link deliveries to a device.
88    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}