use hmac::{Hmac, Mac}; use serde_json::Value; use sha2::{Digest, Sha256}; use std::fmt::Write as _; use crate::pipe::Action; use crate::security::SecurityError; type HmacSha256 = Hmac; pub fn derive_session_key(hmac_seed: &str) -> Result, SecurityError> { let seed = hex::decode(hmac_seed)?; if seed.is_empty() { return Err(SecurityError::InvalidSeed( "hmac_seed must not be empty".to_string(), )); } let mut hasher = Sha256::new(); hasher.update(seed); hasher.update(b"sgclaw-session-v1"); Ok(hasher.finalize().to_vec()) } pub fn sign_command( session_key: &[u8], seq: u64, action: &Action, params: &Value, expected_domain: &str, ) -> Result { if session_key.is_empty() { return Err(SecurityError::InvalidSeed( "session key must not be empty".to_string(), )); } let mut mac = HmacSha256::new_from_slice(session_key) .map_err(|err| SecurityError::Hmac(err.to_string()))?; let canonical = format!( "{}\n{}\n{}\n{}", seq, action.as_str(), stable_json_string(params)?, expected_domain ); mac.update(canonical.as_bytes()); Ok(hex::encode(mac.finalize().into_bytes())) } fn stable_json_string(value: &Value) -> Result { match value { Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => { serde_json::to_string(value).map_err(SecurityError::from) } Value::Array(items) => { let mut out = String::from("["); for (index, item) in items.iter().enumerate() { if index > 0 { out.push(','); } out.push_str(&stable_json_string(item)?); } out.push(']'); Ok(out) } Value::Object(map) => { let mut keys: Vec<&str> = map.keys().map(String::as_str).collect(); keys.sort_unstable(); let mut out = String::from("{"); for (index, key) in keys.iter().enumerate() { if index > 0 { out.push(','); } write!( out, "{}:{}", serde_json::to_string(key)?, stable_json_string(&map[*key])? ) .map_err(|err| SecurityError::Hmac(err.to_string()))?; } out.push('}'); Ok(out) } } }