1use serde::{Deserialize, Serialize};
2
3use crate::error::{CoreError, Result};
4use crate::identity::ChatIdentity;
5use crate::ids::{dm_conversation_id, msg_id_bytes, normalize_pubkey_hex};
6
7pub const MAX_BODY_BYTES: usize = 1800;
10pub const DM_INNER_DOMAIN: &[u8] = b"nysiris-chat-v1/dm-inner";
12pub const DM_INNER_VERSION: u8 = 1;
14
15#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(rename_all = "snake_case")]
18pub enum Direction {
19 Incoming,
21 Outgoing,
23}
24
25#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
27#[serde(rename_all = "snake_case")]
28pub enum MessageState {
29 Sent,
31 Queued,
33 Failed,
35}
36
37#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
40pub struct ChatMessage {
41 pub msg_id: String,
43 pub conversation_id: String,
45 pub from: String,
47 pub to: String,
49 pub body: String,
51 pub at: u64,
53 pub direction: Direction,
55 pub state: MessageState,
57}
58
59impl ChatMessage {
60 pub fn incoming(from: &str, to: &str, msg_id: &str, body: &str, at: u64) -> Self {
62 Self {
63 msg_id: msg_id.to_lowercase(),
64 conversation_id: dm_conversation_id(from),
65 from: from.to_lowercase(),
66 to: to.to_lowercase(),
67 body: body.to_string(),
68 at,
69 direction: Direction::Incoming,
70 state: MessageState::Sent,
71 }
72 }
73
74 pub fn outgoing(from: &str, to: &str, msg_id: &str, body: &str, at: u64) -> Self {
76 Self {
77 msg_id: msg_id.to_lowercase(),
78 conversation_id: dm_conversation_id(to),
79 from: from.to_lowercase(),
80 to: to.to_lowercase(),
81 body: body.to_string(),
82 at,
83 direction: Direction::Outgoing,
84 state: MessageState::Sent,
85 }
86 }
87
88 pub fn incoming_group(from: &str, gid: &str, msg_id: &str, body: &str, at: u64) -> Self {
90 Self {
91 msg_id: msg_id.to_lowercase(),
92 conversation_id: format!("group:{}", gid.to_lowercase()),
93 from: from.to_lowercase(),
94 to: gid.to_lowercase(),
95 body: body.to_string(),
96 at,
97 direction: Direction::Incoming,
98 state: MessageState::Sent,
99 }
100 }
101
102 pub fn outgoing_group(from: &str, gid: &str, msg_id: &str, body: &str, at: u64) -> Self {
104 Self {
105 msg_id: msg_id.to_lowercase(),
106 conversation_id: format!("group:{}", gid.to_lowercase()),
107 from: from.to_lowercase(),
108 to: gid.to_lowercase(),
109 body: body.to_string(),
110 at,
111 direction: Direction::Outgoing,
112 state: MessageState::Sent,
113 }
114 }
115
116 pub fn peer_pub(&self) -> &str {
118 match self.direction {
119 Direction::Incoming => &self.from,
120 Direction::Outgoing => &self.to,
121 }
122 }
123}
124
125#[derive(Clone, Debug, Serialize, Deserialize)]
127pub struct DmInner {
128 pub v: u8,
130 pub from: String,
132 pub to: String,
134 pub ts: u64,
136 #[serde(rename = "msgId")]
138 pub msg_id: String,
139 pub body: String,
141 pub sig: String,
143}
144
145fn inner_sign_bytes(
146 from_hex: &str,
147 to_hex: &str,
148 ts: u64,
149 msg_id_hex: &str,
150 body: &str,
151) -> Result<Vec<u8>> {
152 let from = crate::ids::pubkey_bytes(from_hex)?;
153 let to = crate::ids::pubkey_bytes(to_hex)?;
154 let msg_id = msg_id_bytes(msg_id_hex)?;
155 let mut out = Vec::with_capacity(DM_INNER_DOMAIN.len() + 32 + 32 + 8 + 16 + body.len());
156 out.extend_from_slice(DM_INNER_DOMAIN);
157 out.extend_from_slice(&from);
158 out.extend_from_slice(&to);
159 out.extend_from_slice(&ts.to_be_bytes());
160 out.extend_from_slice(&msg_id);
161 out.extend_from_slice(body.as_bytes());
162 Ok(out)
163}
164
165pub fn pack_dm_inner(
167 identity: &ChatIdentity,
168 to_pub_hex: &str,
169 msg_id_hex: &str,
170 ts: u64,
171 body: &str,
172) -> Result<String> {
173 if body.is_empty() {
174 return Err(CoreError::InvalidInput("message body is empty".into()));
175 }
176 if body.len() > MAX_BODY_BYTES {
177 return Err(CoreError::InvalidInput(format!(
178 "message body exceeds {MAX_BODY_BYTES} bytes"
179 )));
180 }
181 let from = identity.public_hex();
182 let to = normalize_pubkey_hex(to_pub_hex)?;
183 msg_id_bytes(msg_id_hex)?;
184 let bytes = inner_sign_bytes(&from, &to, ts, msg_id_hex, body)?;
185 let inner = DmInner {
186 v: DM_INNER_VERSION,
187 from,
188 to,
189 ts,
190 msg_id: msg_id_hex.to_lowercase(),
191 body: body.to_string(),
192 sig: identity.sign_hex(&bytes),
193 };
194 Ok(serde_json::to_string(&inner)?)
195}
196
197pub fn unpack_dm_inner(json: &str, own_pub_hex: &str) -> Result<DmInner> {
199 let inner: DmInner = serde_json::from_str(json)?;
200 if inner.v != DM_INNER_VERSION {
201 return Err(CoreError::InvalidInput(format!(
202 "unsupported DM inner version {}",
203 inner.v
204 )));
205 }
206 let from = normalize_pubkey_hex(&inner.from)?;
207 let to = normalize_pubkey_hex(&inner.to)?;
208 let own = normalize_pubkey_hex(own_pub_hex)?;
209 if to != own {
210 return Err(CoreError::InvalidInput(
211 "DM inner envelope is addressed to another key".into(),
212 ));
213 }
214 if inner.body.is_empty() || inner.body.len() > MAX_BODY_BYTES {
215 return Err(CoreError::InvalidInput(
216 "DM body has an invalid size".into(),
217 ));
218 }
219 msg_id_bytes(&inner.msg_id)?;
220 let bytes = inner_sign_bytes(&from, &to, inner.ts, &inner.msg_id, &inner.body)?;
221 ChatIdentity::verify_hex(&from, &bytes, &inner.sig)?;
222 Ok(DmInner { from, to, ..inner })
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228
229 #[test]
230 fn inner_envelope_round_trips_and_binds_recipient() {
231 let alice = ChatIdentity::from_secret_bytes([1u8; 32]);
232 let bob = ChatIdentity::from_secret_bytes([2u8; 32]);
233 let id = "ab".repeat(16);
234 let packed = pack_dm_inner(&alice, &bob.public_hex(), &id, 123, "hello").unwrap();
235 let opened = unpack_dm_inner(&packed, &bob.public_hex()).unwrap();
236 assert_eq!(opened.body, "hello");
237 assert_eq!(opened.from, alice.public_hex());
238 assert!(unpack_dm_inner(&packed, &alice.public_hex()).is_err());
239 }
240
241 #[test]
242 fn tampered_body_fails_verification() {
243 let alice = ChatIdentity::from_secret_bytes([1u8; 32]);
244 let bob = ChatIdentity::from_secret_bytes([2u8; 32]);
245 let id = "ab".repeat(16);
246 let packed = pack_dm_inner(&alice, &bob.public_hex(), &id, 123, "hello").unwrap();
247 let mut value: serde_json::Value = serde_json::from_str(&packed).unwrap();
248 value["body"] = serde_json::json!("hello!");
249 assert!(unpack_dm_inner(&value.to_string(), &bob.public_hex()).is_err());
250 }
251}