use hmac::{Hmac, KeyInit, Mac};
use sha2::Sha256;
use crate::domain::ir::JsonObject;
pub const INPUT_REQUIRED: &str = "input_required";
#[derive(Clone)]
pub struct StateKey([u8; 32]);
impl StateKey {
#[must_use]
pub const fn from_bytes(bytes: [u8; 32]) -> Self {
Self(bytes)
}
#[must_use]
pub fn seal(&self, state: &str) -> Option<String> {
let mut mac = self.hmac()?;
mac.update(state.as_bytes());
let mac = hex(&mac.finalize().into_bytes());
Some(format!("{mac}.{state}"))
}
#[must_use]
pub fn open<'a>(&self, sealed: &'a str) -> Option<&'a str> {
let (mac, state) = sealed.split_once('.')?;
let expected = unhex(mac)?;
let mut verifier = self.hmac()?;
verifier.update(state.as_bytes());
verifier.verify_slice(&expected).ok()?;
Some(state)
}
fn hmac(&self) -> Option<Hmac<Sha256>> {
<Hmac<Sha256> as KeyInit>::new_from_slice(&self.0).ok()
}
}
impl core::fmt::Debug for StateKey {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str("StateKey(<redacted>)")
}
}
fn hex(bytes: &[u8]) -> String {
use core::fmt::Write as _;
bytes
.iter()
.fold(String::with_capacity(bytes.len() * 2), |mut out, byte| {
let _ = write!(out, "{byte:02x}");
out
})
}
fn unhex(text: &str) -> Option<Vec<u8>> {
if !text.len().is_multiple_of(2) {
return None;
}
text.as_bytes()
.chunks(2)
.map(|pair| {
let pair = core::str::from_utf8(pair).ok()?;
u8::from_str_radix(pair, 16).ok()
})
.collect()
}
#[derive(Debug, Clone, Default)]
pub struct InputRequired {
requests: serde_json::Map<String, serde_json::Value>,
state: Option<String>,
}
impl InputRequired {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn elicit(self, key: impl Into<String>, params: &JsonObject) -> Self {
self.request(key, "elicitation/create", params)
}
#[must_use]
pub fn sample(self, key: impl Into<String>, params: &JsonObject) -> Self {
self.request(key, "sampling/createMessage", params)
}
#[must_use]
pub fn list_roots(self, key: impl Into<String>) -> Self {
self.request(key, "roots/list", &JsonObject::default())
}
#[must_use]
pub fn with_state(mut self, sealed: impl Into<String>) -> Self {
self.state = Some(sealed.into());
self
}
fn request(mut self, key: impl Into<String>, method: &str, params: &JsonObject) -> Self {
let params = serde_json::from_str(params.as_str())
.unwrap_or_else(|_| serde_json::Value::Object(serde_json::Map::new()));
self.requests.insert(
key.into(),
serde_json::json!({ "method": method, "params": params }),
);
self
}
#[must_use]
pub fn into_result(self) -> JsonObject {
let mut result = serde_json::Map::new();
result.insert("resultType".to_owned(), INPUT_REQUIRED.into());
if !self.requests.is_empty() {
result.insert(
"inputRequests".to_owned(),
serde_json::Value::Object(self.requests),
);
}
if let Some(state) = self.state {
result.insert("requestState".to_owned(), state.into());
}
JsonObject::from_value(serde_json::Value::Object(result)).unwrap_or_default()
}
}
#[derive(Debug, Clone, Default)]
pub struct InputRound {
pub(crate) responses: Option<serde_json::Map<String, serde_json::Value>>,
pub(crate) state: Option<String>,
pub(crate) key: Option<StateKey>,
}
impl InputRound {
#[must_use]
pub fn response(&self, key: &str) -> Option<JsonObject> {
let value = self.responses.as_ref()?.get(key)?;
JsonObject::from_value(value.clone()).ok()
}
#[must_use]
pub const fn has_responses(&self) -> bool {
self.responses.is_some()
}
#[must_use]
pub fn state(&self) -> Option<&str> {
self.state.as_deref()
}
#[must_use]
pub fn seal(&self, state: &str) -> Option<String> {
self.key.as_ref().and_then(|key| key.seal(state))
}
}
#[must_use]
pub fn is_input_required(result: &serde_json::Map<String, serde_json::Value>) -> bool {
result.get("resultType").and_then(serde_json::Value::as_str) == Some(INPUT_REQUIRED)
}
#[must_use]
pub fn capability_for(method: &str) -> Option<&'static str> {
match method {
"sampling/createMessage" => Some("sampling"),
"elicitation/create" => Some("elicitation"),
"roots/list" => Some("roots"),
_ => None,
}
}
#[cfg(test)]
pub mod test_keys;
#[cfg(test)]
mod tests;