hns_chat_protocol/
wire.rs1use std::collections::HashSet;
2
3use hns_encoding::{Decoder, Encoder};
4use k256::ecdsa::VerifyingKey;
5
6use crate::ChatProtocolError;
7
8pub const HNS_CHAT_WIRE_VERSION: u8 = 1;
10
11pub const MAX_CHAT_CIPHERTEXT_SIZE: usize = 8 * 1024;
13pub const MAX_CHAT_ACKNOWLEDGEMENT_SIZE: usize = 2 * 1024;
15pub const MAX_CHAT_ENVELOPE_SIZE: usize = 1 + 32 + 32 + 8 + 8 + 3 + MAX_CHAT_CIPHERTEXT_SIZE;
17pub const MAX_CHAT_ACKNOWLEDGEMENT_WIRE_SIZE: usize =
19 1 + 32 + 8 + 3 + MAX_CHAT_ACKNOWLEDGEMENT_SIZE;
20pub const MAX_CHAT_EXPIRATION_WINDOW: u64 = 7 * 24 * 60 * 60;
22
23#[derive(Clone, Debug, Eq, PartialEq)]
24pub struct ChatEnvelopeV1 {
25 pub message_id: [u8; 32],
26 pub recipient_public_key: [u8; 32],
27 pub created_at: u64,
28 pub expires_at: u64,
29 pub gift_wrap: Vec<u8>,
30}
31
32impl ChatEnvelopeV1 {
33 pub fn encode(&self) -> Result<Vec<u8>, ChatProtocolError> {
34 self.validate()?;
35 let mut encoder = Encoder::with_capacity(81_usize.saturating_add(self.gift_wrap.len()));
36 encoder.put_u8(HNS_CHAT_WIRE_VERSION);
37 encoder.put_bytes(&self.message_id);
38 encoder.put_bytes(&self.recipient_public_key);
39 encoder.put_u64_le(self.created_at);
40 encoder.put_u64_le(self.expires_at);
41 encoder.put_varbytes(&self.gift_wrap);
42 let encoded = encoder.into_bytes();
43 if encoded.len() > MAX_CHAT_ENVELOPE_SIZE {
44 return Err(ChatProtocolError::TooLarge {
45 actual: encoded.len(),
46 maximum: MAX_CHAT_ENVELOPE_SIZE,
47 });
48 }
49 Ok(encoded)
50 }
51
52 pub fn decode(input: &[u8]) -> Result<Self, ChatProtocolError> {
53 if input.len() > MAX_CHAT_ENVELOPE_SIZE {
54 return Err(ChatProtocolError::TooLarge {
55 actual: input.len(),
56 maximum: MAX_CHAT_ENVELOPE_SIZE,
57 });
58 }
59 let mut decoder = Decoder::new(input);
60 if decoder.read_u8()? != HNS_CHAT_WIRE_VERSION {
61 return Err(ChatProtocolError::Invalid(
62 "unsupported chat envelope version",
63 ));
64 }
65 let envelope = Self {
66 message_id: decoder.read_array()?,
67 recipient_public_key: decoder.read_array()?,
68 created_at: decoder.read_u64_le()?,
69 expires_at: decoder.read_u64_le()?,
70 gift_wrap: decoder.read_varbytes(MAX_CHAT_CIPHERTEXT_SIZE, "NIP-59 gift wrap")?,
71 };
72 decoder.finish()?;
73 envelope.validate()?;
74 Ok(envelope)
75 }
76
77 pub fn validate(&self) -> Result<(), ChatProtocolError> {
80 if self.message_id.iter().all(|byte| *byte == 0) {
81 return Err(ChatProtocolError::Invalid("message identifier is zero"));
82 }
83 if self.recipient_public_key.iter().all(|byte| *byte == 0) {
84 return Err(ChatProtocolError::Invalid("recipient public key is zero"));
85 }
86 let mut recipient = [0_u8; 33];
87 recipient[0] = 0x02;
88 recipient[1..].copy_from_slice(&self.recipient_public_key);
89 if VerifyingKey::from_sec1_bytes(&recipient).is_err() {
90 return Err(ChatProtocolError::Invalid(
91 "recipient public key is not a valid x-only secp256k1 key",
92 ));
93 }
94 if self.created_at == 0
95 || self.expires_at <= self.created_at
96 || self.expires_at - self.created_at > MAX_CHAT_EXPIRATION_WINDOW
97 {
98 return Err(ChatProtocolError::Invalid(
99 "message timestamps are noncanonical or exceed retention",
100 ));
101 }
102 if self.gift_wrap.is_empty() {
103 return Err(ChatProtocolError::Invalid("NIP-59 gift wrap is empty"));
104 }
105 if self.gift_wrap.len() > MAX_CHAT_CIPHERTEXT_SIZE {
106 return Err(ChatProtocolError::TooLarge {
107 actual: self.gift_wrap.len(),
108 maximum: MAX_CHAT_CIPHERTEXT_SIZE,
109 });
110 }
111 Ok(())
112 }
113}
114
115#[derive(Clone, Debug, Eq, PartialEq)]
116pub struct ChatAcknowledgementV1 {
117 pub message_id: [u8; 32],
118 pub received_at: u64,
119 pub encrypted_receipt: Vec<u8>,
120}
121
122impl ChatAcknowledgementV1 {
123 pub fn encode(&self) -> Result<Vec<u8>, ChatProtocolError> {
124 self.validate()?;
125 let mut encoder =
126 Encoder::with_capacity(42_usize.saturating_add(self.encrypted_receipt.len()));
127 encoder.put_u8(HNS_CHAT_WIRE_VERSION);
128 encoder.put_bytes(&self.message_id);
129 encoder.put_u64_le(self.received_at);
130 encoder.put_varbytes(&self.encrypted_receipt);
131 let encoded = encoder.into_bytes();
132 if encoded.len() > MAX_CHAT_ACKNOWLEDGEMENT_WIRE_SIZE {
133 return Err(ChatProtocolError::TooLarge {
134 actual: encoded.len(),
135 maximum: MAX_CHAT_ACKNOWLEDGEMENT_WIRE_SIZE,
136 });
137 }
138 Ok(encoded)
139 }
140
141 pub fn decode(input: &[u8]) -> Result<Self, ChatProtocolError> {
142 if input.len() > MAX_CHAT_ACKNOWLEDGEMENT_WIRE_SIZE {
143 return Err(ChatProtocolError::TooLarge {
144 actual: input.len(),
145 maximum: MAX_CHAT_ACKNOWLEDGEMENT_WIRE_SIZE,
146 });
147 }
148 let mut decoder = Decoder::new(input);
149 if decoder.read_u8()? != HNS_CHAT_WIRE_VERSION {
150 return Err(ChatProtocolError::Invalid(
151 "unsupported acknowledgement version",
152 ));
153 }
154 let acknowledgement = Self {
155 message_id: decoder.read_array()?,
156 received_at: decoder.read_u64_le()?,
157 encrypted_receipt: decoder
158 .read_varbytes(MAX_CHAT_ACKNOWLEDGEMENT_SIZE, "encrypted acknowledgement")?,
159 };
160 decoder.finish()?;
161 acknowledgement.validate()?;
162 Ok(acknowledgement)
163 }
164
165 pub fn validate(&self) -> Result<(), ChatProtocolError> {
168 if self.message_id.iter().all(|byte| *byte == 0) {
169 return Err(ChatProtocolError::Invalid("message identifier is zero"));
170 }
171 if self.received_at == 0 {
172 return Err(ChatProtocolError::Invalid(
173 "acknowledgement timestamp is zero",
174 ));
175 }
176 if self.encrypted_receipt.is_empty() {
177 return Err(ChatProtocolError::Invalid(
178 "encrypted acknowledgement is empty",
179 ));
180 }
181 if self.encrypted_receipt.len() > MAX_CHAT_ACKNOWLEDGEMENT_SIZE {
182 return Err(ChatProtocolError::TooLarge {
183 actual: self.encrypted_receipt.len(),
184 maximum: MAX_CHAT_ACKNOWLEDGEMENT_SIZE,
185 });
186 }
187 Ok(())
188 }
189}
190
191pub fn validate_unique_message_ids(envelopes: &[ChatEnvelopeV1]) -> Result<(), ChatProtocolError> {
192 let mut identifiers = HashSet::with_capacity(envelopes.len());
193 for envelope in envelopes {
194 envelope.validate()?;
195 if !identifiers.insert(envelope.message_id) {
196 return Err(ChatProtocolError::DuplicateMessageId);
197 }
198 }
199 Ok(())
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205
206 fn envelope() -> ChatEnvelopeV1 {
207 ChatEnvelopeV1 {
208 message_id: [1; 32],
209 recipient_public_key: hex::decode(
210 "17162c921dc4d2518f9a101db33695df1afb56ab82f5ff3e5da6eec3ca5cd917",
211 )
212 .expect("hex")
213 .try_into()
214 .expect("x-only key"),
215 created_at: 1_700_000_000,
216 expires_at: 1_700_000_600,
217 gift_wrap: br#"{\"kind\":1059,\"content\":\"opaque\"}"#.to_vec(),
218 }
219 }
220
221 #[test]
222 fn opaque_envelope_and_acknowledgement_round_trip_strictly() {
223 let envelope = envelope();
224 let encoded = envelope.encode().expect("encode");
225 assert_eq!(ChatEnvelopeV1::decode(&encoded).expect("decode"), envelope);
226 let acknowledgement = ChatAcknowledgementV1 {
227 message_id: envelope.message_id,
228 received_at: envelope.created_at + 10,
229 encrypted_receipt: b"opaque receipt".to_vec(),
230 };
231 let encoded = acknowledgement.encode().expect("encode");
232 assert_eq!(
233 ChatAcknowledgementV1::decode(&encoded).expect("decode"),
234 acknowledgement
235 );
236 let mut trailing = encoded;
237 trailing.push(0);
238 assert!(ChatAcknowledgementV1::decode(&trailing).is_err());
239 }
240
241 #[test]
242 fn limits_timestamps_and_duplicates_fail_closed() {
243 let mut invalid = envelope();
244 invalid.expires_at = invalid.created_at + MAX_CHAT_EXPIRATION_WINDOW + 1;
245 assert!(invalid.encode().is_err());
246 invalid = envelope();
247 invalid.gift_wrap = vec![0; MAX_CHAT_CIPHERTEXT_SIZE + 1];
248 assert!(invalid.encode().is_err());
249 invalid = envelope();
250 invalid.recipient_public_key = [0xff; 32];
251 assert!(invalid.encode().is_err());
252 let first = envelope();
253 let second = first.clone();
254 assert_eq!(
255 validate_unique_message_ids(&[first, second]),
256 Err(ChatProtocolError::DuplicateMessageId)
257 );
258 }
259}