mostro_core/chat/
unwrap.rs1use nostr_sdk::nips::nip44;
9use nostr_sdk::prelude::*;
10
11use crate::error::{MostroError, ServiceError};
12
13pub const CHAT_MAX_CLOCK_SKEW_SECS: u64 = 60;
16
17pub const CHAT_MAX_CONTENT_BYTES: usize = 64 * 1024;
20
21#[derive(Debug, Clone)]
23pub struct ChatMessage {
24 pub content: String,
26 pub sender: PublicKey,
28 pub created_at: Timestamp,
30 pub inner_event_id: EventId,
32 pub outer_event_id: EventId,
34}
35
36pub fn unwrap_chat_message(
48 conv: &Keys,
49 sign_pubkey: &PublicKey,
50 allowed_signers: &[PublicKey],
51 outer: &Event,
52 now: Timestamp,
53) -> Result<ChatMessage, MostroError> {
54 if outer.pubkey != *sign_pubkey {
56 return Err(MostroError::MostroInternalErr(
57 ServiceError::UnexpectedError(
58 "outer event is not authored by the conversation signing key".to_string(),
59 ),
60 ));
61 }
62 if outer.kind != Kind::PrivateDirectMessage {
63 return Err(MostroError::MostroInternalErr(
64 ServiceError::UnexpectedError("outer event is not kind 14".to_string()),
65 ));
66 }
67
68 let mut p_tags = outer.tags.iter().filter(|t| t.kind() == TagKind::p());
70 match (p_tags.next().and_then(|t| t.content()), p_tags.next()) {
71 (Some(pk), None) if pk == conv.public_key().to_hex() => {}
72 _ => {
73 return Err(MostroError::MostroInternalErr(
74 ServiceError::UnexpectedError(
75 "outer event must carry exactly one p tag for this conversation".to_string(),
76 ),
77 ));
78 }
79 }
80
81 if outer.created_at.as_secs() > now.as_secs().saturating_add(CHAT_MAX_CLOCK_SKEW_SECS) {
83 return Err(MostroError::MostroInternalErr(
84 ServiceError::UnexpectedError("outer event is dated too far in the future".to_string()),
85 ));
86 }
87
88 if outer.content.len() > CHAT_MAX_CONTENT_BYTES {
90 return Err(MostroError::MostroInternalErr(
91 ServiceError::UnexpectedError(
92 "encrypted payload exceeds the accepted size".to_string(),
93 ),
94 ));
95 }
96
97 outer.verify().map_err(|e| {
99 MostroError::MostroInternalErr(ServiceError::NostrError(format!(
100 "invalid outer chat signature: {e}"
101 )))
102 })?;
103
104 let decrypted =
106 nip44::decrypt(conv.secret_key(), &conv.public_key(), &outer.content).map_err(|e| {
107 MostroError::MostroInternalErr(ServiceError::DecryptionError(format!(
108 "K_conv decrypt failed: {e}"
109 )))
110 })?;
111
112 let inner = Event::from_json(&decrypted).map_err(|e| {
113 MostroError::MostroInternalErr(ServiceError::NostrError(format!(
114 "malformed inner chat event: {e}"
115 )))
116 })?;
117
118 inner.verify().map_err(|e| {
120 MostroError::MostroInternalErr(ServiceError::NostrError(format!(
121 "invalid inner chat signature: {e}"
122 )))
123 })?;
124 if !allowed_signers.contains(&inner.pubkey) {
125 return Err(MostroError::MostroInternalErr(
126 ServiceError::UnexpectedError(
127 "inner event is signed by a key that is not a party to this conversation"
128 .to_string(),
129 ),
130 ));
131 }
132 if inner.kind != Kind::TextNote {
133 return Err(MostroError::MostroInternalErr(
134 ServiceError::UnexpectedError("inner chat event is not a TextNote".to_string()),
135 ));
136 }
137
138 let skew = inner
140 .created_at
141 .as_secs()
142 .abs_diff(outer.created_at.as_secs());
143 if skew > CHAT_MAX_CLOCK_SKEW_SECS {
144 return Err(MostroError::MostroInternalErr(
145 ServiceError::UnexpectedError(
146 "inner and outer timestamps disagree — stale re-wrap".to_string(),
147 ),
148 ));
149 }
150
151 Ok(ChatMessage {
152 content: inner.content.clone(),
153 sender: inner.pubkey,
154 created_at: inner.created_at,
155 inner_event_id: inner.id,
156 outer_event_id: outer.id,
157 })
158}
159
160pub async fn unwrap_giftwrap_chat_message(
165 shared_keys: &Keys,
166 event: &Event,
167) -> Result<ChatMessage, MostroError> {
168 if event.kind != Kind::GiftWrap {
169 return Err(MostroError::MostroInternalErr(
170 ServiceError::UnexpectedError("event is not a GiftWrap".to_string()),
171 ));
172 }
173
174 let decrypted = nip44::decrypt(shared_keys.secret_key(), &event.pubkey, &event.content)
175 .map_err(|e| {
176 MostroError::MostroInternalErr(ServiceError::DecryptionError(format!(
177 "shared-key decrypt failed: {e}"
178 )))
179 })?;
180
181 let inner = Event::from_json(&decrypted).map_err(|e| {
182 MostroError::MostroInternalErr(ServiceError::NostrError(format!(
183 "malformed inner chat event: {e}"
184 )))
185 })?;
186
187 if inner.kind != Kind::TextNote {
188 return Err(MostroError::MostroInternalErr(
189 ServiceError::UnexpectedError("inner chat event is not a TextNote".to_string()),
190 ));
191 }
192
193 inner.verify().map_err(|e| {
194 MostroError::MostroInternalErr(ServiceError::NostrError(format!(
195 "invalid inner chat signature: {e}"
196 )))
197 })?;
198
199 Ok(ChatMessage {
200 content: inner.content.clone(),
201 sender: inner.pubkey,
202 created_at: inner.created_at,
203 inner_event_id: inner.id,
204 outer_event_id: event.id,
205 })
206}