1use crypto_box::SalsaBox;
2use crypto_box::aead::{Aead, AeadCore, OsRng as CryptoOsRng};
3use ed25519_dalek::{Signer, Verifier};
4use serde::{Deserialize, Serialize};
5use sha2::{Digest, Sha256};
6
7use super::error::CryptoError;
8use super::keys::OwnerKeypair;
9
10#[derive(Serialize, Deserialize, Clone, Debug)]
12pub struct SignedEncryptedEnvelope {
13 pub version: u32,
14 pub sender_owner_id: String,
15 pub sender_sign_public_key: String,
16 pub sender_box_public_key: String,
17 pub recipient_box_public_key: String,
18 pub message_type: String,
19 pub timestamp_unix_ms: u64,
20 pub nonce: String,
21 pub ciphertext: String,
22}
23
24#[derive(Debug)]
26pub struct OpenedMessage {
27 pub sender_owner_id: String,
28 pub sender_sign_public_key: [u8; 32],
29 pub sender_box_public_key: [u8; 32],
30 pub message_type: String,
31 pub timestamp_unix_ms: u64,
32 pub payload: Vec<u8>,
33}
34
35#[derive(Serialize, Deserialize)]
37struct InnerPayload {
38 payload: Vec<u8>,
39 signature: Vec<u8>,
40}
41
42fn canonical_signed_bytes(
47 version: u32,
48 sender_owner_id: &str,
49 sender_box_public_key: &[u8],
50 recipient_box_public_key: &[u8],
51 message_type: &str,
52 timestamp_unix_ms: u64,
53 payload: &[u8],
54) -> Vec<u8> {
55 let mut buf = Vec::new();
56 let sender_owner_id_bytes = sender_owner_id.as_bytes();
57 let message_type_bytes = message_type.as_bytes();
58
59 buf.extend_from_slice(b"mesh-llm-envelope-v1:");
61 buf.extend_from_slice(&version.to_le_bytes());
62 buf.extend_from_slice(&(sender_owner_id_bytes.len() as u64).to_le_bytes());
63 buf.extend_from_slice(sender_owner_id_bytes);
64 buf.extend_from_slice(sender_box_public_key);
65 buf.extend_from_slice(recipient_box_public_key);
66 buf.extend_from_slice(&(message_type_bytes.len() as u64).to_le_bytes());
67 buf.extend_from_slice(message_type_bytes);
68 buf.extend_from_slice(×tamp_unix_ms.to_le_bytes());
69 let payload_hash = Sha256::digest(payload);
72 buf.extend_from_slice(&payload_hash);
73 buf
74}
75
76pub fn seal_message(
78 sender: &OwnerKeypair,
79 recipient_box_public_key: &crypto_box::PublicKey,
80 message_type: &str,
81 payload: &[u8],
82 timestamp_unix_ms: u64,
83) -> Result<SignedEncryptedEnvelope, CryptoError> {
84 let version = 1u32;
85 let sender_owner_id = sender.owner_id();
86 let sender_box_pk = sender.encryption_public_key();
87
88 let signed_bytes = canonical_signed_bytes(
90 version,
91 &sender_owner_id,
92 sender_box_pk.as_bytes(),
93 recipient_box_public_key.as_bytes(),
94 message_type,
95 timestamp_unix_ms,
96 payload,
97 );
98 let signature = sender.signing.sign(&signed_bytes);
99
100 let inner = InnerPayload {
102 payload: payload.to_vec(),
103 signature: signature.to_bytes().to_vec(),
104 };
105 let inner_bytes = serde_json::to_vec(&inner)?;
106
107 let salsa_box = SalsaBox::new(recipient_box_public_key, &sender.encryption);
109 let nonce = SalsaBox::generate_nonce(&mut CryptoOsRng);
110 let ct = salsa_box
111 .encrypt(&nonce, inner_bytes.as_ref())
112 .map_err(|_| CryptoError::VerificationFailed {
113 reason: "encryption failed".into(),
114 })?;
115
116 Ok(SignedEncryptedEnvelope {
117 version,
118 sender_owner_id,
119 sender_sign_public_key: hex::encode(sender.verifying_key().as_bytes()),
120 sender_box_public_key: hex::encode(sender_box_pk.as_bytes()),
121 recipient_box_public_key: hex::encode(recipient_box_public_key.as_bytes()),
122 message_type: message_type.to_string(),
123 timestamp_unix_ms,
124 nonce: hex::encode(nonce),
125 ciphertext: hex::encode(ct),
126 })
127}
128
129pub fn open_message(
131 recipient: &OwnerKeypair,
132 envelope: &SignedEncryptedEnvelope,
133) -> Result<OpenedMessage, CryptoError> {
134 if envelope.version != 1 {
136 return Err(CryptoError::VerificationFailed {
137 reason: format!("unsupported envelope version: {}", envelope.version),
138 });
139 }
140
141 let sender_sign_pk_bytes: [u8; 32] = hex::decode(&envelope.sender_sign_public_key)
143 .map_err(|_| CryptoError::InvalidKeyMaterial {
144 reason: "bad sender signing key hex".into(),
145 })?
146 .try_into()
147 .map_err(|_| CryptoError::InvalidKeyMaterial {
148 reason: "sender signing key must be 32 bytes".into(),
149 })?;
150
151 let sender_box_pk_bytes: [u8; 32] = hex::decode(&envelope.sender_box_public_key)
152 .map_err(|_| CryptoError::InvalidKeyMaterial {
153 reason: "bad sender box key hex".into(),
154 })?
155 .try_into()
156 .map_err(|_| CryptoError::InvalidKeyMaterial {
157 reason: "sender box key must be 32 bytes".into(),
158 })?;
159
160 let recipient_box_pk_bytes: [u8; 32] = hex::decode(&envelope.recipient_box_public_key)
161 .map_err(|_| CryptoError::InvalidKeyMaterial {
162 reason: "bad recipient box key hex".into(),
163 })?
164 .try_into()
165 .map_err(|_| CryptoError::InvalidKeyMaterial {
166 reason: "recipient box key must be 32 bytes".into(),
167 })?;
168
169 let sender_box_pk = crypto_box::PublicKey::from(sender_box_pk_bytes);
170
171 let actual_recipient_box_pk_bytes = *recipient.encryption_public_key().as_bytes();
175 if recipient_box_pk_bytes != actual_recipient_box_pk_bytes {
176 return Err(CryptoError::VerificationFailed {
177 reason: "recipient_box_public_key does not match recipient encryption public key"
178 .into(),
179 });
180 }
181
182 let sender_verifying_key = ed25519_dalek::VerifyingKey::from_bytes(&sender_sign_pk_bytes)
184 .map_err(|_| CryptoError::InvalidSignature)?;
185 let expected_owner_id = crate::keys::owner_id_from_verifying_key(&sender_verifying_key);
186 if envelope.sender_owner_id != expected_owner_id {
187 return Err(CryptoError::VerificationFailed {
188 reason: "sender_owner_id does not match signing public key".into(),
189 });
190 }
191
192 let nonce_bytes = hex::decode(&envelope.nonce).map_err(|_| CryptoError::DecryptionFailed)?;
194 if nonce_bytes.len() != 24 {
195 return Err(CryptoError::DecryptionFailed);
196 }
197 let nonce = crypto_box::Nonce::from_slice(&nonce_bytes);
198 let ct = hex::decode(&envelope.ciphertext).map_err(|_| CryptoError::DecryptionFailed)?;
199
200 let salsa_box = SalsaBox::new(&sender_box_pk, &recipient.encryption);
201 let inner_bytes = salsa_box
202 .decrypt(nonce, ct.as_ref())
203 .map_err(|_| CryptoError::DecryptionFailed)?;
204
205 let inner: InnerPayload =
207 serde_json::from_slice(&inner_bytes).map_err(|_| CryptoError::DecryptionFailed)?;
208
209 let signed_bytes = canonical_signed_bytes(
211 envelope.version,
212 &envelope.sender_owner_id,
213 &sender_box_pk_bytes,
214 &recipient_box_pk_bytes,
215 &envelope.message_type,
216 envelope.timestamp_unix_ms,
217 &inner.payload,
218 );
219
220 let sig_bytes: [u8; 64] = inner
221 .signature
222 .try_into()
223 .map_err(|_| CryptoError::InvalidSignature)?;
224 let signature = ed25519_dalek::Signature::from_bytes(&sig_bytes);
225
226 sender_verifying_key
227 .verify(&signed_bytes, &signature)
228 .map_err(|_| CryptoError::InvalidSignature)?;
229
230 Ok(OpenedMessage {
231 sender_owner_id: expected_owner_id,
232 sender_sign_public_key: sender_sign_pk_bytes,
233 sender_box_public_key: sender_box_pk_bytes,
234 message_type: envelope.message_type.clone(),
235 timestamp_unix_ms: envelope.timestamp_unix_ms,
236 payload: inner.payload,
237 })
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243
244 #[test]
245 fn seal_open_round_trip() {
246 let sender = OwnerKeypair::generate();
247 let recipient = OwnerKeypair::generate();
248
249 let payload = b"hello, mesh-llm!";
250 let timestamp = 1_700_000_000_000u64;
251
252 let envelope = seal_message(
253 &sender,
254 &recipient.encryption_public_key(),
255 "test.message",
256 payload,
257 timestamp,
258 )
259 .unwrap();
260
261 let opened = open_message(&recipient, &envelope).unwrap();
262 assert_eq!(opened.payload, payload);
263 assert_eq!(opened.message_type, "test.message");
264 assert_eq!(opened.timestamp_unix_ms, timestamp);
265 assert_eq!(opened.sender_owner_id, sender.owner_id());
266 }
267
268 #[test]
269 fn wrong_recipient_cannot_decrypt() {
270 let sender = OwnerKeypair::generate();
271 let recipient = OwnerKeypair::generate();
272 let wrong_recipient = OwnerKeypair::generate();
273
274 let envelope = seal_message(
275 &sender,
276 &recipient.encryption_public_key(),
277 "secret",
278 b"classified",
279 0,
280 )
281 .unwrap();
282
283 let result = open_message(&wrong_recipient, &envelope);
284 assert!(result.is_err(), "wrong recipient should fail to decrypt");
285 }
286
287 #[test]
288 fn tampered_ciphertext_fails() {
289 let sender = OwnerKeypair::generate();
290 let recipient = OwnerKeypair::generate();
291
292 let mut envelope = seal_message(
293 &sender,
294 &recipient.encryption_public_key(),
295 "test",
296 b"payload",
297 0,
298 )
299 .unwrap();
300
301 let mut ct_bytes = hex::decode(&envelope.ciphertext).unwrap();
303 if let Some(byte) = ct_bytes.last_mut() {
304 *byte ^= 0xff;
305 }
306 envelope.ciphertext = hex::encode(&ct_bytes);
307
308 let result = open_message(&recipient, &envelope);
309 assert!(result.is_err(), "tampered ciphertext should fail");
310 }
311
312 #[test]
313 fn spoofed_owner_id_rejected() {
314 let sender = OwnerKeypair::generate();
315 let recipient = OwnerKeypair::generate();
316
317 let mut envelope = seal_message(
318 &sender,
319 &recipient.encryption_public_key(),
320 "test",
321 b"payload",
322 0,
323 )
324 .unwrap();
325
326 envelope.sender_owner_id =
328 "0000000000000000000000000000000000000000000000000000000000000000".into();
329
330 let result = open_message(&recipient, &envelope);
331 assert!(
332 matches!(result, Err(CryptoError::VerificationFailed { .. })),
333 "spoofed owner_id should be rejected"
334 );
335 }
336
337 #[test]
338 fn unknown_envelope_version_rejected() {
339 let sender = OwnerKeypair::generate();
340 let recipient = OwnerKeypair::generate();
341
342 let mut envelope = seal_message(
343 &sender,
344 &recipient.encryption_public_key(),
345 "test",
346 b"payload",
347 0,
348 )
349 .unwrap();
350
351 envelope.version = 99;
352
353 let result = open_message(&recipient, &envelope);
354 assert!(
355 matches!(result, Err(CryptoError::VerificationFailed { .. })),
356 "unknown version should be rejected"
357 );
358 }
359
360 #[test]
361 fn mismatched_recipient_key_rejected() {
362 let sender = OwnerKeypair::generate();
363 let recipient = OwnerKeypair::generate();
364
365 let mut envelope = seal_message(
366 &sender,
367 &recipient.encryption_public_key(),
368 "test",
369 b"payload",
370 0,
371 )
372 .unwrap();
373
374 let other = OwnerKeypair::generate();
376 envelope.recipient_box_public_key = hex::encode(other.encryption_public_key().as_bytes());
377
378 let result = open_message(&recipient, &envelope);
379 assert!(
380 matches!(result, Err(CryptoError::VerificationFailed { .. })),
381 "mismatched recipient key should be rejected"
382 );
383 }
384
385 #[test]
386 fn canonical_bytes_length_prefix_variable_fields() {
387 let sender_box_key = [7u8; 32];
388 let recipient_box_key = [9u8; 32];
389
390 let left = canonical_signed_bytes(
391 1,
392 "ab",
393 &sender_box_key,
394 &recipient_box_key,
395 "c",
396 42,
397 b"payload",
398 );
399 let right = canonical_signed_bytes(
400 1,
401 "a",
402 &sender_box_key,
403 &recipient_box_key,
404 "bc",
405 42,
406 b"payload",
407 );
408
409 assert_ne!(left, right, "variable-length fields must be unambiguous");
410 }
411}