Skip to main content

hns_chat_protocol/
wire.rs

1use std::collections::HashSet;
2
3use hns_encoding::{Decoder, Encoder};
4use k256::ecdsa::VerifyingKey;
5
6use crate::ChatProtocolError;
7
8/// Version byte used by both canonical HNS Chat wire values.
9pub const HNS_CHAT_WIRE_VERSION: u8 = 1;
10
11/// Maximum opaque NIP-59 gift-wrap payload.
12pub const MAX_CHAT_CIPHERTEXT_SIZE: usize = 8 * 1024;
13/// Maximum encrypted acknowledgement payload.
14pub const MAX_CHAT_ACKNOWLEDGEMENT_SIZE: usize = 2 * 1024;
15/// Maximum canonical encoded envelope size, including a three-byte CompactSize.
16pub const MAX_CHAT_ENVELOPE_SIZE: usize = 1 + 32 + 32 + 8 + 8 + 3 + MAX_CHAT_CIPHERTEXT_SIZE;
17/// Maximum canonical encoded acknowledgement size, including CompactSize.
18pub const MAX_CHAT_ACKNOWLEDGEMENT_WIRE_SIZE: usize =
19    1 + 32 + 8 + 3 + MAX_CHAT_ACKNOWLEDGEMENT_SIZE;
20/// Maximum difference between envelope creation and expiration timestamps.
21pub 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    /// Validate a programmatically constructed envelope without allocating an
78    /// encoded copy.
79    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    /// Validate a programmatically constructed acknowledgement without
166    /// allocating an encoded copy.
167    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}