use std::collections::BTreeSet;
use affinidi_messaging_didcomm::{DIDCommAgent, Message, UnpackResult};
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine as _;
use serde::{de::DeserializeOwned, Serialize};
use trust_tasks_rs::{Payload, TrustTask};
use crate::error::DidcommError;
use crate::handler::DidcommHandler;
pub const ENVELOPE_TYPE: &str = "https://trusttasks.org/binding/didcomm/0.1/envelope";
pub fn pack_trust_task<P>(
doc: &TrustTask<P>,
agent: &DIDCommAgent,
sender_did: &str,
recipient_did: &str,
) -> Result<String, DidcommError>
where
P: Payload + Serialize,
{
let body = serde_json::to_value(doc).map_err(DidcommError::SerialiseBody)?;
let thid = doc.thread_id.clone().unwrap_or_else(|| doc.id.clone());
let mut msg = Message::new(ENVELOPE_TYPE, body)
.from(sender_did.to_string())
.to(vec![recipient_did.to_string()])
.thid(thid);
if let Some(parent) = doc.parent_thread_id.clone() {
msg = msg.pthid(parent);
}
let wire = agent.pack_authcrypt(&msg, sender_did, recipient_did)?;
Ok(wire)
}
pub fn unpack_trust_task<P>(
wire: &str,
agent: &DIDCommAgent,
expected_sender_did: Option<&str>,
) -> Result<(TrustTask<P>, DidcommHandler), DidcommError>
where
P: Payload + DeserializeOwned,
{
let (message, peer_did, local_did) = match agent.unpack(wire, expected_sender_did)? {
UnpackResult::Encrypted {
message,
authenticated: true,
sender_kid: Some(sender_kid),
recipient_kid,
..
} => {
let peer = sender_did_from_kid(&sender_kid)?;
if let Some(expected) = expected_sender_did {
if expected != peer {
return Err(DidcommError::SenderKidMismatch {
expected: expected.to_string(),
advertised: peer,
});
}
}
(
message,
Some(peer),
Some(did_from_kid(&recipient_kid).unwrap_or(recipient_kid)),
)
}
UnpackResult::Encrypted { .. } | UnpackResult::Plaintext(_) => {
return Err(DidcommError::UnauthenticatedSender);
}
UnpackResult::Signed { .. } => {
return Err(DidcommError::SignedNotAuthcrypted);
}
_ => return Err(DidcommError::UnauthenticatedSender),
};
if message.typ != ENVELOPE_TYPE {
return Err(DidcommError::WrongEnvelopeType(message.typ.clone()));
}
let doc: TrustTask<P> =
serde_json::from_value(message.body).map_err(DidcommError::InvalidBody)?;
check_thread(
"thid",
"threadId",
message.thid.as_deref(),
doc.thread_id.as_deref(),
)?;
check_thread(
"pthid",
"parentThreadId",
message.pthid.as_deref(),
doc.parent_thread_id.as_deref(),
)?;
let handler = DidcommHandler::new(local_did, peer_did);
Ok((doc, handler))
}
fn check_thread(
header: &'static str,
member: &'static str,
transport: Option<&str>,
in_band: Option<&str>,
) -> Result<(), DidcommError> {
match (transport, in_band) {
(Some(t), Some(b)) if t != b => Err(DidcommError::ThreadMismatch {
header,
member,
transport: t.to_string(),
in_band: b.to_string(),
}),
_ => Ok(()),
}
}
fn did_from_kid(kid: &str) -> Option<String> {
kid.split_once('#').map(|(did, _)| did.to_string())
}
fn sender_did_from_kid(kid: &str) -> Result<String, DidcommError> {
did_from_kid(kid).ok_or_else(|| DidcommError::UnqualifiedSenderKid {
kid: kid.to_string(),
})
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SenderAllowlist {
allowed: BTreeSet<String>,
}
impl SenderAllowlist {
pub fn new<I, S>(dids: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
Self {
allowed: dids.into_iter().map(Into::into).collect(),
}
}
pub fn from_agent_peers(agent: &DIDCommAgent) -> Self {
Self::new(agent.store().resolved_dids())
}
pub fn allow(mut self, did: impl Into<String>) -> Self {
self.allowed.insert(did.into());
self
}
pub fn permits(&self, did: &str) -> bool {
self.allowed.contains(did)
}
pub fn allowed(&self) -> impl Iterator<Item = &str> {
self.allowed.iter().map(String::as_str)
}
pub fn is_empty(&self) -> bool {
self.allowed.is_empty()
}
}
pub fn advertised_sender_did(wire: &str) -> Result<String, DidcommError> {
let envelope: serde_json::Value = serde_json::from_str(wire)
.map_err(|e| DidcommError::NotAuthcryptJwe(format!("not JSON: {e}")))?;
let protected = envelope
.get("protected")
.and_then(serde_json::Value::as_str)
.ok_or_else(|| DidcommError::NotAuthcryptJwe("no `protected` header".into()))?;
let decoded = URL_SAFE_NO_PAD
.decode(protected)
.map_err(|e| DidcommError::NotAuthcryptJwe(format!("`protected` is not base64url: {e}")))?;
let header: serde_json::Value = serde_json::from_slice(&decoded)
.map_err(|e| DidcommError::NotAuthcryptJwe(format!("`protected` is not JSON: {e}")))?;
let skid = header
.get("skid")
.and_then(serde_json::Value::as_str)
.ok_or_else(|| DidcommError::NotAuthcryptJwe("no `skid` (not authcrypt)".into()))?;
sender_did_from_kid(skid)
}
pub fn unpack_trust_task_from<P>(
wire: &str,
agent: &DIDCommAgent,
allowlist: &SenderAllowlist,
) -> Result<(TrustTask<P>, DidcommHandler), DidcommError>
where
P: Payload + DeserializeOwned,
{
let advertised = advertised_sender_did(wire)?;
if !allowlist.permits(&advertised) {
return Err(DidcommError::SenderNotAllowed { did: advertised });
}
unpack_trust_task(wire, agent, Some(&advertised))
}
#[cfg(test)]
mod thread_tests {
use super::*;
#[test]
fn absent_on_either_side_is_not_a_mismatch() {
assert!(check_thread("thid", "threadId", None, Some("a")).is_ok());
assert!(check_thread("thid", "threadId", Some("a"), None).is_ok());
assert!(check_thread("thid", "threadId", None, None).is_ok());
}
#[test]
fn equal_values_pass() {
assert!(check_thread("thid", "threadId", Some("a"), Some("a")).is_ok());
}
#[test]
fn disagreement_is_a_malformed_request() {
let err = check_thread("pthid", "parentThreadId", Some("outer"), Some("other"))
.expect_err("both present and different must fail");
assert!(matches!(
err,
DidcommError::ThreadMismatch {
header: "pthid",
member: "parentThreadId",
..
}
));
match err.into_reject_reason() {
trust_tasks_rs::RejectReason::MalformedRequest { .. } => {}
other => panic!("expected malformedRequest, got {other:?}"),
}
}
}