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