use serde::{Deserialize, Serialize};
use super::crypto::{
canonical_typedid_conversation, hex_encode, random_nonce, sha256_tagged, unix_time,
};
use super::document::DidResolver;
use super::error::DidError;
use super::gateway::{VerifiedDidPrompt, VerifiedTypeDidMessage};
use super::identifier::Did;
use super::keystore::DidKeyStore;
use super::typedid::{TypeDidConversation, TypeDidMode};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DidMessageBody {
pub action: String,
pub resource: String,
pub privacy: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reply_to: Option<DidMessageReference>,
}
impl DidMessageBody {
pub fn infer_prompt(resource: impl Into<String>) -> Self {
Self {
action: "ai:infer".to_owned(),
resource: resource.into(),
privacy: "secret".to_owned(),
reply_to: None,
}
}
pub fn reply_to_prompt(prompt: &VerifiedDidPrompt) -> Self {
Self {
action: prompt.body.action.clone(),
resource: prompt.body.resource.clone(),
privacy: prompt.body.privacy.clone(),
reply_to: Some(prompt.prompt_ref.clone()),
}
}
pub fn agent_message(resource: impl Into<String>, privacy: impl Into<String>) -> Self {
Self {
action: "agent:message".to_owned(),
resource: resource.into(),
privacy: privacy.into(),
reply_to: None,
}
}
pub fn agent_delegate(resource: impl Into<String>, privacy: impl Into<String>) -> Self {
Self {
action: "agent:delegate".to_owned(),
resource: resource.into(),
privacy: privacy.into(),
reply_to: None,
}
}
}
#[derive(Debug, Clone)]
pub struct DidReplyBinding {
pub prompt_body: DidMessageBody,
pub prompt_ref: DidMessageReference,
}
impl DidReplyBinding {
pub fn for_prompt(prompt: &VerifiedDidPrompt) -> Self {
Self {
prompt_body: prompt.body.clone(),
prompt_ref: prompt.prompt_ref.clone(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DidMessageReference {
pub id: String,
pub digest: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DidEnvelope {
pub id: String,
#[serde(rename = "type")]
pub message_type: String,
pub from: Did,
pub to: Vec<Did>,
pub created_time: u64,
pub expires_time: u64,
pub body: DidMessageBody,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub typedid: Option<TypeDidConversation>,
pub kid: String,
pub nonce: String,
pub ciphertext: String,
pub signature: String,
}
impl DidEnvelope {
pub fn prompt(
id: impl Into<String>,
from: Did,
to: Did,
body: DidMessageBody,
plaintext: impl AsRef<[u8]>,
resolver: &dyn DidResolver,
key_store: &dyn DidKeyStore,
) -> Result<Self, DidError> {
let id = id.into();
let now = unix_time();
let recipient_document = resolver.resolve(&to)?;
let recipient_key = recipient_document.key_agreement_key()?;
let recipient_public = recipient_key.public_key()?;
let sender_document = resolver.resolve(&from)?;
let kid = sender_document
.authentication
.first()
.cloned()
.ok_or(DidError::MissingAuthentication)?;
let nonce = random_nonce()?;
let mut envelope = Self {
id,
message_type: "https://typesec.dev/did/message/v1/prompt".to_owned(),
from,
to: vec![to],
created_time: now,
expires_time: now + 300,
body,
typedid: None,
kid,
nonce: hex_encode(&nonce),
ciphertext: String::new(),
signature: String::new(),
};
let aad = envelope.associated_data();
envelope.ciphertext = key_store.encrypt_for(
&envelope.from,
&recipient_public,
plaintext.as_ref(),
&nonce,
&aad,
)?;
envelope.signature = key_store.sign(&envelope.from, envelope.signing_input().as_bytes())?;
Ok(envelope)
}
pub fn reply(
reply_did: Did,
from: Did,
to: Did,
binding: DidReplyBinding,
plaintext: impl AsRef<[u8]>,
resolver: &dyn DidResolver,
key_store: &dyn DidKeyStore,
) -> Result<Self, DidError> {
let DidReplyBinding {
prompt_body,
prompt_ref,
} = binding;
let now = unix_time();
let recipient_document = resolver.resolve(&to)?;
let recipient_key = recipient_document.key_agreement_key()?;
let recipient_public = recipient_key.public_key()?;
let sender_document = resolver.resolve(&from)?;
let kid = sender_document
.authentication
.first()
.cloned()
.ok_or(DidError::MissingAuthentication)?;
let id = reply_did.to_string();
let nonce = random_nonce()?;
let mut envelope = Self {
id,
message_type: "https://typesec.dev/did/message/v1/reply".to_owned(),
from,
to: vec![to],
created_time: now,
expires_time: now + 300,
body: DidMessageBody {
action: prompt_body.action.clone(),
resource: prompt_body.resource.clone(),
privacy: prompt_body.privacy.clone(),
reply_to: Some(prompt_ref),
},
typedid: None,
kid,
nonce: hex_encode(&nonce),
ciphertext: String::new(),
signature: String::new(),
};
let aad = envelope.associated_data();
envelope.ciphertext = key_store.encrypt_for(
&envelope.from,
&recipient_public,
plaintext.as_ref(),
&nonce,
&aad,
)?;
envelope.signature = key_store.sign(&envelope.from, envelope.signing_input().as_bytes())?;
Ok(envelope)
}
#[allow(clippy::too_many_arguments)]
pub fn typedid(
id: impl Into<String>,
from: Did,
to: Did,
body: DidMessageBody,
typedid: TypeDidConversation,
plaintext: impl AsRef<[u8]>,
resolver: &dyn DidResolver,
key_store: &dyn DidKeyStore,
) -> Result<Self, DidError> {
let mut envelope = Self::prompt(id, from, to, body, plaintext, resolver, key_store)?;
envelope.message_type = "https://typesec.dev/did/message/v1/typedid".to_owned();
envelope.typedid = Some(typedid);
envelope.signature = key_store.sign(&envelope.from, envelope.signing_input().as_bytes())?;
Ok(envelope)
}
pub fn typedid_reply(
id: impl Into<String>,
from: Did,
to: Did,
request: &VerifiedTypeDidMessage,
plaintext: impl AsRef<[u8]>,
resolver: &dyn DidResolver,
key_store: &dyn DidKeyStore,
) -> Result<Self, DidError> {
let mut body = request.body.clone();
body.reply_to = Some(request.message_ref.clone());
let conversation = TypeDidConversation {
conversation_id: request.conversation.conversation_id.clone(),
mode: TypeDidMode::RequestReply,
profile: request.conversation.profile.clone(),
protocol: request.conversation.protocol.clone(),
expires_at: request.conversation.expires_at,
};
Self::typedid(
id,
from,
to,
body,
conversation,
plaintext,
resolver,
key_store,
)
}
pub fn reference(&self) -> DidMessageReference {
let seed = format!("{}\n{}", self.signing_input(), self.signature);
DidMessageReference {
id: self.id.clone(),
digest: hex_encode(&sha256_tagged(
b"typesec-did-envelope-reference",
seed.as_bytes(),
)),
}
}
pub(super) fn associated_data(&self) -> Vec<u8> {
format!(
"{}\n{}\n{}\n{}\n{}",
self.id,
self.from,
self.to
.iter()
.map(Did::as_str)
.collect::<Vec<_>>()
.join(","),
self.created_time,
self.expires_time,
)
.into_bytes()
}
pub(super) fn signing_input(&self) -> String {
let reply_to = self
.body
.reply_to
.as_ref()
.map(|reference| format!("{}\n{}", reference.id, reference.digest))
.unwrap_or_default();
let base = format!(
"{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}",
self.id,
self.message_type,
self.from,
self.to
.iter()
.map(Did::as_str)
.collect::<Vec<_>>()
.join(","),
self.created_time,
self.expires_time,
self.body.action,
self.body.resource,
self.body.privacy,
reply_to,
self.kid,
self.nonce,
);
if let Some(typedid) = self.typedid.as_ref() {
format!(
"{}\n{}\n{}",
base,
canonical_typedid_conversation(typedid),
self.ciphertext
)
} else {
format!("{}\n{}", base, self.ciphertext)
}
}
}