botrs 0.11.0

A Rust QQ Bot framework based on QQ Guild Bot API
Documentation
//! Botgo-compatible ed25519 signature helpers for interaction callbacks.

#![allow(non_snake_case, non_upper_case_globals)]

use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
use reqwest::header::HeaderMap;

pub const HeaderSig: &str = "X-Signature-Ed25519";
pub const HeaderTimestamp: &str = "X-Signature-Timestamp";

fn seed(secret: &str) -> crate::Result<[u8; 32]> {
    if secret.is_empty() {
        return Err(crate::BotError::invalid_data("secret invalid"));
    }

    let mut repeated = secret.as_bytes().to_vec();
    while repeated.len() < 32 {
        repeated.extend_from_slice(secret.as_bytes());
    }

    let mut seed = [0u8; 32];
    seed.copy_from_slice(&repeated[..32]);
    Ok(seed)
}

fn original_content(timestamp: &str, body: &[u8]) -> crate::Result<Vec<u8>> {
    if timestamp.is_empty() {
        return Err(crate::BotError::invalid_data("timestamp is nil"));
    }
    let mut content = Vec::with_capacity(timestamp.len() + body.len());
    content.extend_from_slice(timestamp.as_bytes());
    content.extend_from_slice(body);
    Ok(content)
}

fn header_value(headers: &HeaderMap, name: &str) -> String {
    headers
        .get(name)
        .and_then(|value| value.to_str().ok())
        .unwrap_or_default()
        .to_string()
}

pub fn Generate(secret: &str, headers: &HeaderMap, http_body: &[u8]) -> crate::Result<String> {
    let key = SigningKey::from_bytes(&seed(secret)?);
    let content = original_content(&header_value(headers, HeaderTimestamp), http_body)?;
    Ok(hex::encode(key.sign(&content).to_bytes()))
}

pub fn Verify(secret: &str, headers: &HeaderMap, http_body: &[u8]) -> crate::Result<bool> {
    let signature = header_value(headers, HeaderSig);
    if signature.is_empty() {
        return Err(crate::BotError::invalid_data("not found signature"));
    }

    let signature = hex::decode(signature).map_err(|err| {
        crate::BotError::invalid_data(format!("hex decode signature failed: {err}"))
    })?;
    let signature = Signature::from_slice(&signature)
        .map_err(|_| crate::BotError::invalid_data("signature decode result is not a valid buf"))?;
    let verifying_key = VerifyingKey::from(&SigningKey::from_bytes(&seed(secret)?));
    let content = original_content(&header_value(headers, HeaderTimestamp), http_body)?;

    Ok(verifying_key.verify(&content, &signature).is_ok())
}

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

    #[test]
    fn generated_signature_verifies() {
        let mut headers = HeaderMap::new();
        headers.insert(HeaderTimestamp, "123456".parse().unwrap());
        let body = br#"{"hello":"world"}"#;

        let signature = Generate("secret", &headers, body).unwrap();
        headers.insert(HeaderSig, signature.parse().unwrap());

        assert!(Verify("secret", &headers, body).unwrap());
        assert!(!Verify("secret", &headers, b"changed").unwrap());
    }
}