guardian-shared 0.13.3

Shared types and utilities for Guardian
Documentation
use miden_protocol::crypto::hash::rpo::Rpo256;
use miden_protocol::{Felt, FieldElement, Word};
use serde::Serialize;
use serde_json::Value;

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AuthRequestPayload {
    digest: Word,
}

impl AuthRequestPayload {
    pub fn empty() -> Self {
        Self {
            digest: Word::from([Felt::ZERO; 4]),
        }
    }

    pub fn from_bytes(bytes: &[u8]) -> Self {
        if bytes.is_empty() {
            return Self::empty();
        }

        let mut payload_elements = Vec::with_capacity(bytes.len().div_ceil(8));
        for chunk in bytes.chunks(8) {
            let mut chunk_bytes = [0u8; 8];
            chunk_bytes[..chunk.len()].copy_from_slice(chunk);
            payload_elements.push(Felt::new(u64::from_le_bytes(chunk_bytes)));
        }

        Self {
            digest: Rpo256::hash_elements(&payload_elements),
        }
    }

    pub fn from_json_bytes(bytes: &[u8]) -> Result<Self, String> {
        let value: Value =
            serde_json::from_slice(bytes).map_err(|e| format!("Invalid JSON payload: {e}"))?;
        Self::from_json_value(&value)
    }

    pub fn from_json_value(value: &Value) -> Result<Self, String> {
        let canonical = canonicalize_json(value);
        let bytes =
            serde_json::to_vec(&canonical).map_err(|e| format!("Failed to serialize JSON: {e}"))?;
        Ok(Self::from_bytes(&bytes))
    }

    pub fn from_json_serializable<T: Serialize>(value: &T) -> Result<Self, String> {
        let json = serde_json::to_value(value)
            .map_err(|e| format!("Failed to convert payload to JSON value: {e}"))?;
        Self::from_json_value(&json)
    }

    pub fn from_protobuf_message<T: prost::Message>(value: &T) -> Self {
        Self::from_bytes(&value.encode_to_vec())
    }

    pub fn as_elements(&self) -> [Felt; 4] {
        let elements = self.digest.as_elements();
        [elements[0], elements[1], elements[2], elements[3]]
    }

    pub fn to_word(&self) -> Word {
        self.digest
    }
}

fn canonicalize_json(value: &Value) -> Value {
    match value {
        Value::Object(map) => {
            let mut keys = map.keys().cloned().collect::<Vec<_>>();
            keys.sort();

            let mut sorted = serde_json::Map::with_capacity(map.len());
            for key in keys {
                let item = map
                    .get(&key)
                    .expect("key collected from map must always exist");
                sorted.insert(key, canonicalize_json(item));
            }
            Value::Object(sorted)
        }
        Value::Array(items) => Value::Array(items.iter().map(canonicalize_json).collect()),
        _ => value.clone(),
    }
}

#[cfg(test)]
mod tests {
    use super::AuthRequestPayload;

    #[test]
    fn json_payload_hash_is_order_insensitive() {
        let left = br#"{"b":2,"a":1}"#;
        let right = br#"{"a":1,"b":2}"#;

        let left_payload = AuthRequestPayload::from_json_bytes(left).expect("left json");
        let right_payload = AuthRequestPayload::from_json_bytes(right).expect("right json");

        assert_eq!(left_payload, right_payload);
    }

    #[test]
    fn empty_payload_hash_is_zero_word() {
        let payload = AuthRequestPayload::from_bytes(b"");
        assert_eq!(payload, AuthRequestPayload::empty());
    }
}