1use serde::{Deserialize, Serialize};
4
5use super::crypto::{
6 canonical_typedid_conversation, hex_encode, random_nonce, sha256_tagged, unix_time,
7};
8use super::document::DidResolver;
9use super::error::DidError;
10use super::gateway::{VerifiedDidPrompt, VerifiedTypeDidMessage};
11use super::identifier::Did;
12use super::keystore::DidKeyStore;
13use super::typedid::{TypeDidConversation, TypeDidMode};
14
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
17pub struct DidMessageBody {
18 pub action: String,
20 pub resource: String,
22 pub privacy: String,
24 #[serde(default, skip_serializing_if = "Option::is_none")]
26 pub reply_to: Option<DidMessageReference>,
27}
28
29impl DidMessageBody {
30 pub fn infer_prompt(resource: impl Into<String>) -> Self {
32 Self {
33 action: "ai:infer".to_owned(),
34 resource: resource.into(),
35 privacy: "secret".to_owned(),
36 reply_to: None,
37 }
38 }
39
40 pub fn reply_to_prompt(prompt: &VerifiedDidPrompt) -> Self {
42 Self {
43 action: prompt.body.action.clone(),
44 resource: prompt.body.resource.clone(),
45 privacy: prompt.body.privacy.clone(),
46 reply_to: Some(prompt.prompt_ref.clone()),
47 }
48 }
49
50 pub fn agent_message(resource: impl Into<String>, privacy: impl Into<String>) -> Self {
52 Self {
53 action: "agent:message".to_owned(),
54 resource: resource.into(),
55 privacy: privacy.into(),
56 reply_to: None,
57 }
58 }
59
60 pub fn agent_delegate(resource: impl Into<String>, privacy: impl Into<String>) -> Self {
62 Self {
63 action: "agent:delegate".to_owned(),
64 resource: resource.into(),
65 privacy: privacy.into(),
66 reply_to: None,
67 }
68 }
69}
70
71#[derive(Debug, Clone)]
73pub struct DidReplyBinding {
74 pub prompt_body: DidMessageBody,
76 pub prompt_ref: DidMessageReference,
78}
79
80impl DidReplyBinding {
81 pub fn for_prompt(prompt: &VerifiedDidPrompt) -> Self {
83 Self {
84 prompt_body: prompt.body.clone(),
85 prompt_ref: prompt.prompt_ref.clone(),
86 }
87 }
88}
89
90#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
92pub struct DidMessageReference {
93 pub id: String,
95 pub digest: String,
97}
98
99#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
101pub struct DidEnvelope {
102 pub id: String,
104 #[serde(rename = "type")]
106 pub message_type: String,
107 pub from: Did,
109 pub to: Vec<Did>,
111 pub created_time: u64,
113 pub expires_time: u64,
115 pub body: DidMessageBody,
117 #[serde(default, skip_serializing_if = "Option::is_none")]
119 pub typedid: Option<TypeDidConversation>,
120 pub kid: String,
122 pub nonce: String,
124 pub ciphertext: String,
126 pub signature: String,
128}
129
130impl DidEnvelope {
131 #[allow(clippy::too_many_arguments)]
140 fn seal(
141 id: String,
142 message_type: &str,
143 from: Did,
144 to: Did,
145 body: DidMessageBody,
146 typedid: Option<TypeDidConversation>,
147 plaintext: &[u8],
148 resolver: &dyn DidResolver,
149 key_store: &dyn DidKeyStore,
150 ) -> Result<Self, DidError> {
151 let now = unix_time();
152 let recipient_document = resolver.resolve(&to)?;
153 let recipient_public = recipient_document.key_agreement_key()?.public_key()?;
154 let sender_document = resolver.resolve(&from)?;
155 let kid = sender_document
156 .authentication
157 .first()
158 .cloned()
159 .ok_or(DidError::MissingAuthentication)?;
160 let nonce = random_nonce()?;
161 let mut envelope = Self {
164 id,
165 message_type: message_type.to_owned(),
166 from,
167 to: vec![to],
168 created_time: now,
169 expires_time: now + 300,
170 body,
171 typedid,
172 kid,
173 nonce: hex_encode(&nonce),
174 ciphertext: String::new(),
175 signature: String::new(),
176 };
177 let aad = envelope.associated_data();
178 envelope.ciphertext =
179 key_store.encrypt_for(&envelope.from, &recipient_public, plaintext, &nonce, &aad)?;
180 envelope.signature = key_store.sign(&envelope.from, envelope.signing_input().as_bytes())?;
181 Ok(envelope)
182 }
183
184 pub fn prompt(
186 id: impl Into<String>,
187 from: Did,
188 to: Did,
189 body: DidMessageBody,
190 plaintext: impl AsRef<[u8]>,
191 resolver: &dyn DidResolver,
192 key_store: &dyn DidKeyStore,
193 ) -> Result<Self, DidError> {
194 Self::seal(
195 id.into(),
196 "https://typesec.dev/did/message/v1/prompt",
197 from,
198 to,
199 body,
200 None,
201 plaintext.as_ref(),
202 resolver,
203 key_store,
204 )
205 }
206
207 pub fn reply(
209 reply_did: Did,
210 from: Did,
211 to: Did,
212 binding: DidReplyBinding,
213 plaintext: impl AsRef<[u8]>,
214 resolver: &dyn DidResolver,
215 key_store: &dyn DidKeyStore,
216 ) -> Result<Self, DidError> {
217 let DidReplyBinding {
218 prompt_body,
219 prompt_ref,
220 } = binding;
221 Self::seal(
222 reply_did.to_string(),
223 "https://typesec.dev/did/message/v1/reply",
224 from,
225 to,
226 DidMessageBody {
227 action: prompt_body.action.clone(),
228 resource: prompt_body.resource.clone(),
229 privacy: prompt_body.privacy.clone(),
230 reply_to: Some(prompt_ref),
231 },
232 None,
233 plaintext.as_ref(),
234 resolver,
235 key_store,
236 )
237 }
238
239 #[allow(clippy::too_many_arguments)]
241 pub fn typedid(
242 id: impl Into<String>,
243 from: Did,
244 to: Did,
245 body: DidMessageBody,
246 typedid: TypeDidConversation,
247 plaintext: impl AsRef<[u8]>,
248 resolver: &dyn DidResolver,
249 key_store: &dyn DidKeyStore,
250 ) -> Result<Self, DidError> {
251 Self::seal(
252 id.into(),
253 "https://typesec.dev/did/message/v1/typedid",
254 from,
255 to,
256 body,
257 Some(typedid),
258 plaintext.as_ref(),
259 resolver,
260 key_store,
261 )
262 }
263
264 pub fn typedid_reply(
266 id: impl Into<String>,
267 from: Did,
268 to: Did,
269 request: &VerifiedTypeDidMessage,
270 plaintext: impl AsRef<[u8]>,
271 resolver: &dyn DidResolver,
272 key_store: &dyn DidKeyStore,
273 ) -> Result<Self, DidError> {
274 let mut body = request.body.clone();
275 body.reply_to = Some(request.message_ref.clone());
276 let conversation = TypeDidConversation {
277 conversation_id: request.conversation.conversation_id.clone(),
278 mode: TypeDidMode::RequestReply,
279 profile: request.conversation.profile.clone(),
280 protocol: request.conversation.protocol.clone(),
281 expires_at: request.conversation.expires_at,
282 };
283 Self::typedid(
284 id,
285 from,
286 to,
287 body,
288 conversation,
289 plaintext,
290 resolver,
291 key_store,
292 )
293 }
294
295 pub fn reference(&self) -> DidMessageReference {
297 let seed = format!("{}\n{}", self.signing_input(), self.signature);
298 DidMessageReference {
299 id: self.id.clone(),
300 digest: hex_encode(&sha256_tagged(
301 b"typesec-did-envelope-reference",
302 seed.as_bytes(),
303 )),
304 }
305 }
306
307 pub(super) fn associated_data(&self) -> Vec<u8> {
317 format!(
318 "{}\n{}\n{}\n{}\n{}",
319 self.id,
320 self.from,
321 self.to
322 .iter()
323 .map(Did::as_str)
324 .collect::<Vec<_>>()
325 .join(","),
326 self.created_time,
327 self.expires_time,
328 )
329 .into_bytes()
330 }
331
332 pub(super) fn signing_input(&self) -> String {
339 let reply_to = self
340 .body
341 .reply_to
342 .as_ref()
343 .map(|reference| format!("{}\n{}", reference.id, reference.digest))
344 .unwrap_or_default();
345 let base = format!(
346 "{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}",
347 self.id,
348 self.message_type,
349 self.from,
350 self.to
351 .iter()
352 .map(Did::as_str)
353 .collect::<Vec<_>>()
354 .join(","),
355 self.created_time,
356 self.expires_time,
357 self.body.action,
358 self.body.resource,
359 self.body.privacy,
360 reply_to,
361 self.kid,
362 self.nonce,
363 );
364 if let Some(typedid) = self.typedid.as_ref() {
365 format!(
366 "{}\n{}\n{}",
367 base,
368 canonical_typedid_conversation(typedid),
369 self.ciphertext
370 )
371 } else {
372 format!("{}\n{}", base, self.ciphertext)
373 }
374 }
375}