1mod state;
8mod types;
9pub use state::*;
10pub use types::*;
11#[cfg(feature = "http-client")]
12mod client;
13#[cfg(feature = "http-client")]
14pub use client::MailClient;
15
16use crate::identity::{self, AgentId, AgentSigner, Envelope, Event};
17use crate::{Result, SdkError};
18use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
19use hpke::{
20 aead::ChaCha20Poly1305, kdf::HkdfSha256, kem::X25519HkdfSha256, Deserializable, Kem as _,
21 OpModeR, OpModeS, Serializable,
22};
23use serde::Serialize;
24use serde_json::{json, Value};
25use sha3::{Digest, Sha3_256};
26use std::{
27 collections::BTreeMap,
28 sync::{Mutex, OnceLock},
29};
30use zeroize::Zeroizing;
31
32pub const PROTOCOL: &str = "agent-mail/1.0";
33pub const MAX_TTL_MS: i64 = 30 * 86_400_000;
34pub const FUTURE_SKEW_MS: i64 = 300_000;
35pub const MAX_PACKET_BYTES: usize = 1_048_576;
36pub const SCHEMA_JSON: &str = include_str!("schema.json");
37const INFO: &[u8] = PROTOCOL.as_bytes();
39type Kem = X25519HkdfSha256;
40type PrivateKey = <Kem as hpke::Kem>::PrivateKey;
41type PublicKey = <Kem as hpke::Kem>::PublicKey;
42
43pub(crate) fn fail(code: &'static str, message: impl Into<String>) -> SdkError {
44 SdkError::protocol(code, message)
45}
46pub fn canonical_bytes<T: Serialize>(value: &T) -> Result<Vec<u8>> {
47 serde_jcs::to_vec(value).map_err(|e| SdkError::CanonicalJson(e.to_string()))
48}
49pub fn hash_bytes(bytes: &[u8]) -> String {
50 URL_SAFE_NO_PAD.encode(Sha3_256::digest(bytes))
51}
52pub fn encode_bytes(bytes: &[u8]) -> String {
53 URL_SAFE_NO_PAD.encode(bytes)
54}
55pub fn decode_bytes(value: &str) -> Result<Vec<u8>> {
56 let bytes = URL_SAFE_NO_PAD
57 .decode(value)
58 .map_err(|_| fail("invalid_event", "noncanonical base64url"))?;
59 if URL_SAFE_NO_PAD.encode(&bytes) != value {
60 return Err(fail("invalid_event", "noncanonical base64url"));
61 }
62 Ok(bytes)
63}
64pub(crate) fn fixed_bytes(value: &str, size: usize) -> Result<Vec<u8>> {
65 let bytes = decode_bytes(value)?;
66 if bytes.len() != size {
67 return Err(fail("invalid_event", "invalid encoded byte length"));
68 }
69 Ok(bytes)
70}
71pub fn random_id() -> Result<String> {
72 let mut bytes = [0; 16];
73 getrandom::fill(&mut bytes).map_err(|e| SdkError::Random(e.to_string()))?;
74 Ok(encode_bytes(&bytes))
75}
76pub fn parse_mail_address(value: &str) -> Result<MailAddress> {
78 let url = identity::parse_agent_url(value)?;
79 let mailbox_id = match (url.protocol.as_deref(), url.resource) {
80 (Some("mail"), Some(mailbox_id)) => mailbox_id,
81 _ => return Err(fail("invalid_url", "not a mailbox address")),
82 };
83 fixed_bytes(&mailbox_id, 16).map_err(|_| fail("invalid_url", "mailbox_id must be an id16"))?;
84 Ok(MailAddress {
85 owner: url.agent_id,
86 mailbox_id,
87 routes: url.routes,
88 })
89}
90pub fn format_mail_address(owner: &AgentId, mailbox_id: &str, routes: &[String]) -> Result<String> {
92 fixed_bytes(mailbox_id, 16).map_err(|_| fail("invalid_url", "mailbox_id must be an id16"))?;
93 identity::format_agent_url(&identity::AgentUrl {
94 agent_id: owner.clone(),
95 protocol: Some("mail".into()),
96 resource: Some(mailbox_id.into()),
97 routes: routes.to_vec(),
98 })
99}
100pub fn validate_mail_schema(value: &Value, definition: &str) -> Result<()> {
102 static VALIDATORS: OnceLock<Mutex<BTreeMap<String, jsonschema::Validator>>> = OnceLock::new();
103 let mut validators = VALIDATORS
104 .get_or_init(|| Mutex::new(BTreeMap::new()))
105 .lock()
106 .map_err(|_| fail("invalid_event", "schema cache unavailable"))?;
107 if !validators.contains_key(definition) {
108 let schema: Value = serde_json::from_str(SCHEMA_JSON)?;
109 if schema["$defs"].get(definition).is_none() {
110 return Err(fail("invalid_event", "unknown Mail schema definition"));
111 }
112 let validator = jsonschema::options()
113 .with_draft(jsonschema::Draft::Draft202012)
114 .build(&json!({"$ref":format!("#/$defs/{definition}"),"$defs":schema["$defs"]}))
115 .map_err(|e| fail("invalid_event", e.to_string()))?;
116 validators.insert(definition.into(), validator);
117 }
118 validators[definition]
119 .validate(value)
120 .map_err(|e| fail("invalid_event", e.to_string()))
121}
122pub(crate) fn normalized(value: &Value) -> Value {
123 match value {
124 Value::Number(n) if n.as_i64().is_none() && n.as_u64().is_none() => n
125 .as_f64()
126 .filter(|f| f.fract() == 0.0 && f.abs() <= identity::MAX_SAFE_NONCE as f64)
127 .map(|f| json!(f as i64))
128 .unwrap_or(value.clone()),
129 Value::Array(a) => Value::Array(a.iter().map(normalized).collect()),
130 Value::Object(o) => {
131 Value::Object(o.iter().map(|(k, v)| (k.clone(), normalized(v))).collect())
132 }
133 _ => value.clone(),
134 }
135}
136fn signed(value: &Value, definition: &str) -> Result<Envelope<Value>> {
137 identity::parse_strict_json(&serde_json::to_string(value)?)?;
138 validate_mail_schema(value, definition)?;
139 let envelope: Envelope<Value> = serde_json::from_value(normalized(value))?;
140 identity::validate_event_fields(&envelope.event, &[])?;
141 identity::validate_nonce(envelope.event.nonce)?;
142 identity::verify_envelope(&envelope)?;
143 Ok(envelope)
144}
145pub(crate) fn lifetime(created: i64, expires: i64) -> Result<()> {
146 if created < 0
147 || expires <= created
148 || expires > created.saturating_add(MAX_TTL_MS)
149 || expires as u64 > identity::MAX_SAFE_NONCE
150 {
151 return Err(fail("invalid_event", "invalid lifetime"));
152 }
153 Ok(())
154}
155pub(crate) fn clock(now: i64) -> Result<()> {
156 if now < 0 || now as u64 > identity::MAX_SAFE_NONCE {
157 return Err(fail("invalid_request", "invalid clock"));
158 }
159 Ok(())
160}
161pub fn validate_card(value: &Value) -> Result<MailboxCard> {
163 let envelope = signed(value, "mailboxCardEnvelope")?;
164 let card: MailboxCard = serde_json::from_value(serde_json::to_value(envelope)?)?;
165 let p = &card.event.payload;
166 lifetime(card.event.created_at, p.expires_at)?;
167 if p.receive_until < p.expires_at || p.receive_until > p.expires_at.saturating_add(MAX_TTL_MS) {
168 return Err(fail("invalid_event", "invalid receive_until"));
169 }
170 for route in &p.routes {
171 identity::validate_origin(route)?;
172 }
173 let probe_enc = <Kem as hpke::Kem>::EncappedKey::from_bytes(&fixed_bytes(&p.public_key, 32)?)
174 .map_err(|_| fail("invalid_event", "invalid X25519 key"))?;
175 let probe =
178 PrivateKey::from_bytes(&[42; 32]).map_err(|_| fail("invalid_event", "invalid key"))?;
179 Kem::decap(&probe, None, &probe_enc)
180 .map_err(|_| fail("invalid_event", "unusable X25519 key"))?;
181 Ok(card)
182}
183pub fn validate_card_for_sending(value: &Value, owner: &AgentId, now: i64) -> Result<MailboxCard> {
184 let card = validate_card(value)?;
185 check_card_usable(&card, owner, now)?;
186 Ok(card)
187}
188pub(crate) fn check_card_usable(card: &MailboxCard, owner: &AgentId, now: i64) -> Result<()> {
189 clock(now)?;
190 if &card.event.actor != owner {
191 return Err(fail("invalid_actor", "unexpected mailbox owner"));
192 }
193 if card.event.created_at > now.saturating_add(FUTURE_SKEW_MS) {
194 return Err(SdkError::TimestampOutOfWindow);
195 }
196 if card.event.payload.routes.is_empty() {
197 return Err(fail("mailbox_unavailable", "mailbox closed"));
198 }
199 if now >= card.event.payload.expires_at {
200 return Err(fail("stale_card", "card expired"));
201 }
202 Ok(())
203}
204pub fn validate_message(value: &Value) -> Result<MessagePayload> {
206 identity::parse_strict_json(&serde_json::to_string(value)?)?;
207 validate_mail_schema(value, "messagePayload")?;
208 let p: MessagePayload = serde_json::from_value(normalized(value))?;
209 lifetime(p.created_at, p.expires_at)?;
210 for part in &p.parts {
211 let bytes = decode_bytes(&part.data)?;
212 if part.media_type.starts_with("text/") {
213 std::str::from_utf8(&bytes)
214 .map_err(|_| fail("invalid_event", "text part is not UTF-8"))?;
215 }
216 }
217 if let Some(reply) = &p.reply_card {
218 if validate_card(reply)?.event.actor != p.sender {
219 return Err(fail(
220 "invalid_actor",
221 "reply card owner differs from sender",
222 ));
223 }
224 }
225 Ok(p)
226}
227pub fn parse_card(text: &str) -> Result<MailboxCard> {
228 validate_card(&identity::parse_strict_json(text)?)
229}
230pub fn parse_message(text: &str) -> Result<MessagePayload> {
231 validate_message(&identity::parse_strict_json(text)?)
232}
233pub fn sign_card(
234 signer: &AgentSigner,
235 payload: MailboxCardPayload,
236 created_at: i64,
237 nonce: u64,
238) -> Result<MailboxCard> {
239 let envelope = signer.sign_event(Event::new(
240 PROTOCOL,
241 "mailbox.publish",
242 signer.agent_id(),
243 created_at,
244 nonce,
245 payload,
246 ))?;
247 validate_card(&serde_json::to_value(&envelope)?)?;
248 Ok(envelope)
249}
250pub fn create_mail_message(
251 actor: &AgentId,
252 created_at: i64,
253 mut payload: Value,
254) -> Result<MessagePayload> {
255 let fields = payload
256 .as_object_mut()
257 .ok_or_else(|| fail("invalid_event", "message content must be an object"))?;
258 if ["message_id", "from", "created_at"]
259 .iter()
260 .any(|k| fields.contains_key(*k))
261 {
262 return Err(fail(
263 "invalid_event",
264 "message content overrides identity fields",
265 ));
266 }
267 fields.insert("message_id".into(), json!(random_id()?));
268 fields.insert("from".into(), json!(actor));
269 fields.insert("created_at".into(), json!(created_at));
270 validate_message(&payload)
271}
272pub fn validate_reply(reply: &MessagePayload, parent: &MessagePayload) -> Result<()> {
274 validate_message(&serde_json::to_value(reply)?)?;
275 validate_message(&serde_json::to_value(parent)?)?;
276 if reply.to != parent.sender
277 || parent.to != reply.sender
278 || reply.in_reply_to.as_deref() != Some(parent.message_id.as_str())
279 || reply.thread_id != parent.thread_id
280 {
281 return Err(fail("invalid_event", "reply binding mismatch"));
282 }
283 Ok(())
284}
285pub struct MailEncryptionKey(PrivateKey);
288impl MailEncryptionKey {
289 pub fn generate() -> Result<Self> {
290 let mut bytes = Zeroizing::new([0; 32]);
291 getrandom::fill(bytes.as_mut()).map_err(|e| SdkError::Random(e.to_string()))?;
292 Self::from_secret(bytes.as_ref())
293 }
294 pub fn from_secret(bytes: &[u8]) -> Result<Self> {
295 Ok(Self(PrivateKey::from_bytes(bytes).map_err(|_| {
296 fail("invalid_private_key", "X25519 secret must contain 32 bytes")
297 })?))
298 }
299 pub fn public_key(&self) -> String {
300 encode_bytes(&Kem::sk_to_pk(&self.0).to_bytes())
301 }
302 pub fn export_secret(&self) -> Zeroizing<Vec<u8>> {
303 Zeroizing::new(self.0.to_bytes().to_vec())
304 }
305}
306pub fn frame_bytes(bytes: &[u8]) -> Result<Zeroizing<Vec<u8>>> {
308 if bytes.is_empty() || bytes.len() > MAX_PACKET_BYTES {
309 return Err(fail("invalid_event", "invalid frame length"));
310 }
311 let size = (4 + bytes.len()).div_ceil(1024) * 1024;
312 let mut frame = Zeroizing::new(vec![0; size]);
313 frame[..4].copy_from_slice(&(bytes.len() as u32).to_be_bytes());
314 frame[4..4 + bytes.len()].copy_from_slice(bytes);
315 Ok(frame)
316}
317fn unframe(bytes: &[u8]) -> Result<Value> {
318 if bytes.len() < 1024 || bytes.len() % 1024 != 0 {
319 return Err(fail("invalid_event", "invalid frame length"));
320 }
321 let n = u32::from_be_bytes(bytes[..4].try_into().unwrap()) as usize;
322 if n == 0
323 || n > bytes.len() - 4
324 || (4 + n).div_ceil(1024) * 1024 != bytes.len()
325 || bytes[4 + n..].iter().any(|b| *b != 0)
326 {
327 return Err(fail("invalid_event", "invalid frame padding"));
328 }
329 let data = &bytes[4..4 + n];
330 let text = std::str::from_utf8(data).map_err(|_| fail("invalid_event", "invalid UTF-8"))?;
331 let value = identity::parse_strict_json(text)?;
332 if canonical_bytes(&value)? != data {
333 return Err(fail("invalid_event", "noncanonical plaintext"));
334 }
335 Ok(value)
336}
337pub(crate) fn checked_packet(value: &Value) -> Result<(Packet, Vec<u8>)> {
339 let envelope = signed(value, "packetEnvelope")?;
340 let packet: Packet = serde_json::from_value(serde_json::to_value(envelope)?)?;
341 let e = &packet.event;
342 let p = &e.payload;
343 fixed_bytes(&p.enc, 32)?;
344 let n = decode_bytes(&p.ciphertext)?.len();
345 if n < 1040
346 || n % 1024 != 16
347 || p.header.expires_at <= e.created_at
348 || p.header.expires_at > e.created_at.saturating_add(MAX_TTL_MS + FUTURE_SKEW_MS)
349 {
350 return Err(fail(
351 "invalid_event",
352 "invalid ciphertext length or packet lifetime",
353 ));
354 }
355 let bytes = canonical_bytes(&packet)?;
356 if bytes.len() > MAX_PACKET_BYTES {
357 return Err(fail("payload_too_large", "packet too large"));
358 }
359 Ok((packet, bytes))
360}
361pub fn validate_packet(value: &Value) -> Result<Packet> {
362 Ok(checked_packet(value)?.0)
363}
364pub fn parse_packet(text: &str) -> Result<Packet> {
365 if text.len() > 2 * MAX_PACKET_BYTES {
366 return Err(fail("payload_too_large", "raw packet too large"));
367 }
368 validate_packet(&identity::parse_strict_json(text)?)
369}
370pub fn packet_id(packet: &Packet) -> Result<String> {
371 checked_packet(&serde_json::to_value(packet)?)?;
372 Ok(packet.hash.clone())
373}
374pub fn mail_packet_aad(event: &Event<PacketPayload>) -> Result<Vec<u8>> {
376 canonical_bytes(
377 &json!({"protocol":event.protocol,"type":event.kind,"actor":event.actor,
378 "created_at":event.created_at,"nonce":event.nonce,"header":event.payload.header}),
379 )
380}
381pub fn encrypt_message(
383 message: &MessagePayload,
384 card: &MailboxCard,
385 signer: &AgentSigner,
386 nonce: u64,
387 now: i64,
388) -> Result<Packet> {
389 let message = validate_message(&serde_json::to_value(message)?)?;
390 if message.sender != signer.agent_id() {
391 return Err(fail("invalid_actor", "message sender differs from signer"));
392 }
393 identity::validate_nonce(nonce)?;
394 validate_card_for_sending(&serde_json::to_value(card)?, &message.to, now)?;
395 if message.created_at > now.saturating_add(FUTURE_SKEW_MS) || message.expires_at <= now {
396 return Err(fail("packet_expired", "message expired or future-dated"));
397 }
398 if message.expires_at > card.event.payload.receive_until {
399 return Err(fail("invalid_event", "message exceeds receive_until"));
400 }
401 let plaintext = frame_bytes(&Zeroizing::new(canonical_bytes(&message)?))?;
402 let mut event = Event::new(
403 PROTOCOL,
404 "mail.submit",
405 signer.agent_id(),
406 now,
407 nonce,
408 PacketPayload {
409 header: PacketHeader {
410 mailbox_id: card.event.payload.mailbox_id.clone(),
411 card_hash: card.hash.clone(),
412 expires_at: message.expires_at,
413 },
414 enc: encode_bytes(&[0; 32]),
415 ciphertext: encode_bytes(&vec![0; plaintext.len() + 16]),
416 },
417 );
418 let projected = Envelope {
419 event: event.clone(),
420 hash: "A".repeat(43),
421 signature: "A".repeat(86),
422 };
423 if canonical_bytes(&projected)?.len() > card.event.payload.max_packet_bytes {
424 return Err(fail("payload_too_large", "packet exceeds card limit"));
425 }
426 let pk = PublicKey::from_bytes(&fixed_bytes(&card.event.payload.public_key, 32)?)
427 .map_err(|_| fail("invalid_event", "invalid encryption key"))?;
428 let (enc, ciphertext) = hpke::single_shot_seal::<ChaCha20Poly1305, HkdfSha256, Kem>(
429 &OpModeS::Base,
430 &pk,
431 INFO,
432 &plaintext,
433 &mail_packet_aad(&event)?,
434 )
435 .map_err(|_| fail("invalid_event", "encryption failed"))?;
436 event.payload.enc = encode_bytes(&enc.to_bytes());
437 event.payload.ciphertext = encode_bytes(&ciphertext);
438 signer.sign_event(event)
439}
440pub fn decrypt_packet(
442 packet: &Packet,
443 card: &MailboxCard,
444 key: &MailEncryptionKey,
445 owner: &AgentId,
446 now: i64,
447) -> Result<MessagePayload> {
448 clock(now)?;
449 let (packet, bytes) = checked_packet(&serde_json::to_value(packet)?)?;
450 let card = validate_card(&serde_json::to_value(card)?)?;
451 open_verified(&packet, &bytes, &card, key, owner, now)
452}
453pub(crate) fn open_verified(
454 packet: &Packet,
455 bytes: &[u8],
456 card: &MailboxCard,
457 key: &MailEncryptionKey,
458 owner: &AgentId,
459 now: i64,
460) -> Result<MessagePayload> {
461 let e = &packet.event;
462 let h = &e.payload.header;
463 let p = &card.event.payload;
464 if e.created_at > now.saturating_add(FUTURE_SKEW_MS) {
465 return Err(SdkError::TimestampOutOfWindow);
466 }
467 if &card.event.actor != owner {
468 return Err(fail("invalid_actor", "card owner mismatch"));
469 }
470 if bytes.len() > p.max_packet_bytes {
471 return Err(fail("payload_too_large", "packet exceeds card limit"));
472 }
473 if key.public_key() != p.public_key {
474 return Err(fail("invalid_private_key", "recipient key mismatch"));
475 }
476 if h.card_hash != card.hash || h.mailbox_id != p.mailbox_id || h.expires_at > p.receive_until {
477 return Err(fail("invalid_event", "recipient/card/key binding"));
478 }
479 let enc = <Kem as hpke::Kem>::EncappedKey::from_bytes(&fixed_bytes(&e.payload.enc, 32)?)
480 .map_err(|_| fail("invalid_event", "invalid encapsulated key"))?;
481 let plaintext = Zeroizing::new(
482 hpke::single_shot_open::<ChaCha20Poly1305, HkdfSha256, Kem>(
483 &OpModeR::Base,
484 &key.0,
485 &enc,
486 INFO,
487 &decode_bytes(&e.payload.ciphertext)?,
488 &mail_packet_aad(e)?,
489 )
490 .map_err(|_| fail("invalid_event", "unable to decrypt packet"))?,
491 );
492 let message = validate_message(&unframe(&plaintext)?)?;
493 if message.sender != e.actor || &message.to != owner || message.expires_at != h.expires_at {
494 return Err(fail(
495 "invalid_event",
496 "sender, recipient or expiration binding",
497 ));
498 }
499 if message.created_at > now.min(e.created_at).saturating_add(FUTURE_SKEW_MS) {
500 return Err(SdkError::TimestampOutOfWindow);
501 }
502 Ok(message)
503}
504pub fn validate_discovery(value: &Value, origin: &str) -> Result<()> {
506 validate_mail_schema(value, "discoveryDocument")?;
507 identity::validate_origin(origin)?;
508 if value["service"] != origin {
509 return Err(fail("invalid_response", "discovery origin mismatch"));
510 }
511 Ok(())
512}
513pub(crate) fn owner_claims(
514 token: &str,
515 origin: &str,
516 now: i64,
517) -> Result<identity::RequestJwtClaims> {
518 clock(now)?;
519 identity::verify_request_jwt(
520 token,
521 &identity::RequestAuthContext {
522 audience: origin.into(),
523 now_secs: now / 1000,
524 max_ttl_secs: identity::DEFAULT_REQUEST_JWT_TTL_SECS,
525 },
526 )
527 .map_err(|e| fail("invalid_token", e.to_string()))
528}
529pub fn verify_owner_jwt(token: &str, owner: &AgentId, origin: &str, now: i64) -> Result<()> {
531 identity::validate_origin(origin)?;
532 if &owner_claims(token, origin, now)?.iss != owner {
533 return Err(SdkError::PermissionDenied);
534 }
535 Ok(())
536}