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 pub fn prompt(
133 id: impl Into<String>,
134 from: Did,
135 to: Did,
136 body: DidMessageBody,
137 plaintext: impl AsRef<[u8]>,
138 resolver: &dyn DidResolver,
139 key_store: &dyn DidKeyStore,
140 ) -> Result<Self, DidError> {
141 let id = id.into();
142 let now = unix_time();
143 let recipient_document = resolver.resolve(&to)?;
144 let recipient_key = recipient_document.key_agreement_key()?;
145 let recipient_public = recipient_key.public_key()?;
146 let sender_document = resolver.resolve(&from)?;
147 let kid = sender_document
148 .authentication
149 .first()
150 .cloned()
151 .ok_or(DidError::MissingAuthentication)?;
152 let nonce = random_nonce()?;
153 let mut envelope = Self {
156 id,
157 message_type: "https://typesec.dev/did/message/v1/prompt".to_owned(),
158 from,
159 to: vec![to],
160 created_time: now,
161 expires_time: now + 300,
162 body,
163 typedid: None,
164 kid,
165 nonce: hex_encode(&nonce),
166 ciphertext: String::new(),
167 signature: String::new(),
168 };
169 let aad = envelope.associated_data();
170 envelope.ciphertext = key_store.encrypt_for(
171 &envelope.from,
172 &recipient_public,
173 plaintext.as_ref(),
174 &nonce,
175 &aad,
176 )?;
177 envelope.signature = key_store.sign(&envelope.from, envelope.signing_input().as_bytes())?;
178 Ok(envelope)
179 }
180
181 pub fn reply(
183 reply_did: Did,
184 from: Did,
185 to: Did,
186 binding: DidReplyBinding,
187 plaintext: impl AsRef<[u8]>,
188 resolver: &dyn DidResolver,
189 key_store: &dyn DidKeyStore,
190 ) -> Result<Self, DidError> {
191 let DidReplyBinding {
192 prompt_body,
193 prompt_ref,
194 } = binding;
195 let now = unix_time();
196 let recipient_document = resolver.resolve(&to)?;
197 let recipient_key = recipient_document.key_agreement_key()?;
198 let recipient_public = recipient_key.public_key()?;
199 let sender_document = resolver.resolve(&from)?;
200 let kid = sender_document
201 .authentication
202 .first()
203 .cloned()
204 .ok_or(DidError::MissingAuthentication)?;
205 let id = reply_did.to_string();
206 let nonce = random_nonce()?;
207 let mut envelope = Self {
208 id,
209 message_type: "https://typesec.dev/did/message/v1/reply".to_owned(),
210 from,
211 to: vec![to],
212 created_time: now,
213 expires_time: now + 300,
214 body: DidMessageBody {
215 action: prompt_body.action.clone(),
216 resource: prompt_body.resource.clone(),
217 privacy: prompt_body.privacy.clone(),
218 reply_to: Some(prompt_ref),
219 },
220 typedid: None,
221 kid,
222 nonce: hex_encode(&nonce),
223 ciphertext: String::new(),
224 signature: String::new(),
225 };
226 let aad = envelope.associated_data();
227 envelope.ciphertext = key_store.encrypt_for(
228 &envelope.from,
229 &recipient_public,
230 plaintext.as_ref(),
231 &nonce,
232 &aad,
233 )?;
234 envelope.signature = key_store.sign(&envelope.from, envelope.signing_input().as_bytes())?;
235 Ok(envelope)
236 }
237
238 #[allow(clippy::too_many_arguments)]
240 pub fn typedid(
241 id: impl Into<String>,
242 from: Did,
243 to: Did,
244 body: DidMessageBody,
245 typedid: TypeDidConversation,
246 plaintext: impl AsRef<[u8]>,
247 resolver: &dyn DidResolver,
248 key_store: &dyn DidKeyStore,
249 ) -> Result<Self, DidError> {
250 let mut envelope = Self::prompt(id, from, to, body, plaintext, resolver, key_store)?;
251 envelope.message_type = "https://typesec.dev/did/message/v1/typedid".to_owned();
252 envelope.typedid = Some(typedid);
253 envelope.signature = key_store.sign(&envelope.from, envelope.signing_input().as_bytes())?;
254 Ok(envelope)
255 }
256
257 pub fn typedid_reply(
259 id: impl Into<String>,
260 from: Did,
261 to: Did,
262 request: &VerifiedTypeDidMessage,
263 plaintext: impl AsRef<[u8]>,
264 resolver: &dyn DidResolver,
265 key_store: &dyn DidKeyStore,
266 ) -> Result<Self, DidError> {
267 let mut body = request.body.clone();
268 body.reply_to = Some(request.message_ref.clone());
269 let conversation = TypeDidConversation {
270 conversation_id: request.conversation.conversation_id.clone(),
271 mode: TypeDidMode::RequestReply,
272 profile: request.conversation.profile.clone(),
273 protocol: request.conversation.protocol.clone(),
274 expires_at: request.conversation.expires_at,
275 };
276 Self::typedid(
277 id,
278 from,
279 to,
280 body,
281 conversation,
282 plaintext,
283 resolver,
284 key_store,
285 )
286 }
287
288 pub fn reference(&self) -> DidMessageReference {
290 let seed = format!("{}\n{}", self.signing_input(), self.signature);
291 DidMessageReference {
292 id: self.id.clone(),
293 digest: hex_encode(&sha256_tagged(
294 b"typesec-did-envelope-reference",
295 seed.as_bytes(),
296 )),
297 }
298 }
299
300 pub(super) fn associated_data(&self) -> Vec<u8> {
311 format!(
312 "{}\n{}\n{}\n{}\n{}",
313 self.id,
314 self.from,
315 self.to
316 .iter()
317 .map(Did::as_str)
318 .collect::<Vec<_>>()
319 .join(","),
320 self.created_time,
321 self.expires_time,
322 )
323 .into_bytes()
324 }
325
326 pub(super) fn signing_input(&self) -> String {
333 let reply_to = self
334 .body
335 .reply_to
336 .as_ref()
337 .map(|reference| format!("{}\n{}", reference.id, reference.digest))
338 .unwrap_or_default();
339 let base = format!(
340 "{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}",
341 self.id,
342 self.message_type,
343 self.from,
344 self.to
345 .iter()
346 .map(Did::as_str)
347 .collect::<Vec<_>>()
348 .join(","),
349 self.created_time,
350 self.expires_time,
351 self.body.action,
352 self.body.resource,
353 self.body.privacy,
354 reply_to,
355 self.kid,
356 self.nonce,
357 );
358 if let Some(typedid) = self.typedid.as_ref() {
359 format!(
360 "{}\n{}\n{}",
361 base,
362 canonical_typedid_conversation(typedid),
363 self.ciphertext
364 )
365 } else {
366 format!("{}\n{}", base, self.ciphertext)
367 }
368 }
369}