use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
pub fn verify_signature(
signing_secret: &str,
body: &[u8],
timestamp: &str,
signature: &str,
) -> bool {
let sig = match signature.strip_prefix("v0=") {
Some(s) => s,
None => return false,
};
let base_string = format!("v0:{}:{}", timestamp, String::from_utf8_lossy(body));
let expected = hmac_sha256(signing_secret.as_bytes(), base_string.as_bytes());
let expected_hex = hex::encode(expected);
let Ok(expected_bytes) = hex::decode(sig) else {
return false;
};
let Ok(computed_bytes) = hex::decode(&expected_hex) else {
return false;
};
if expected_bytes.len() != computed_bytes.len() {
return false;
}
let mut diff: u8 = 0;
for (a, b) in expected_bytes.iter().zip(computed_bytes.iter()) {
diff |= a ^ b;
}
diff == 0
}
fn hmac_sha256(key: &[u8], message: &[u8]) -> [u8; 32] {
const BLOCK_SIZE: usize = 64;
let key_block = if key.len() > BLOCK_SIZE {
let hash = Sha256::digest(key);
let mut block = [0u8; BLOCK_SIZE];
block[..32].copy_from_slice(&hash);
block
} else {
let mut block = [0u8; BLOCK_SIZE];
block[..key.len()].copy_from_slice(key);
block
};
let mut inner_hasher = Sha256::new();
let mut ipad = [0u8; BLOCK_SIZE];
for (i, k) in key_block.iter().enumerate() {
ipad[i] = k ^ 0x36;
}
inner_hasher.update(ipad);
inner_hasher.update(message);
let inner_result = inner_hasher.finalize();
let mut outer_hasher = Sha256::new();
let mut opad = [0u8; BLOCK_SIZE];
for (i, k) in key_block.iter().enumerate() {
opad[i] = k ^ 0x5c;
}
outer_hasher.update(opad);
outer_hasher.update(inner_result);
let outer_result = outer_hasher.finalize();
let mut output = [0u8; 32];
output.copy_from_slice(&outer_result);
output
}
#[derive(Debug, Deserialize, Serialize)]
pub struct SlackChallenge {
pub challenge: String,
pub token: String,
#[serde(rename = "type")]
pub event_type: String,
}
#[derive(Debug, Deserialize)]
pub struct SlackEventPayload {
pub token: String,
pub team_id: String,
pub api_app_id: String,
pub event: serde_json::Value,
#[serde(rename = "type")]
pub payload_type: String,
pub event_id: Option<String>,
pub event_time: Option<i64>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn verify_signature_valid() {
let secret = "test_signing_secret";
let body = r#"{"type":"url_verification","challenge":"abc123"}"#;
let timestamp = "1234567890";
let base_string = format!("v0:{timestamp}:{body}");
let mac = hmac_sha256(secret.as_bytes(), base_string.as_bytes());
let signature = format!("v0={}", hex::encode(mac));
assert!(verify_signature(
secret,
body.as_bytes(),
timestamp,
&signature
));
}
#[test]
fn verify_signature_invalid_body() {
let secret = "test_signing_secret";
let body = r#"{"type":"url_verification","challenge":"abc123"}"#;
let timestamp = "1234567890";
let base_string = format!("v0:{timestamp}:{body}");
let mac = hmac_sha256(secret.as_bytes(), base_string.as_bytes());
let signature = format!("v0={}", hex::encode(mac));
assert!(!verify_signature(
secret,
b"tampered body",
timestamp,
&signature
));
}
#[test]
fn verify_signature_wrong_prefix() {
assert!(!verify_signature(
"secret",
b"body",
"123",
"invalid_prefix=abc"
));
}
#[test]
fn verify_signature_malformed_hex() {
assert!(!verify_signature(
"secret",
b"body",
"123",
"v0=not_valid_hex!!"
));
}
#[test]
fn hmac_sha256_consistency() {
let key = b"my_secret";
let msg = b"hello world";
let a = hmac_sha256(key, msg);
let b = hmac_sha256(key, msg);
assert_eq!(a, b);
}
#[test]
fn hmac_sha256_key_longer_than_block() {
let key = [0xAB_u8; 100];
let msg = b"test message";
let result = hmac_sha256(&key, msg);
assert_ne!(result, [0u8; 32]);
}
#[test]
fn deserialize_challenge() {
let json = r#"{"challenge":"abc123","token":"xyz","type":"url_verification"}"#;
let challenge: SlackChallenge = serde_json::from_str(json).unwrap();
assert_eq!(challenge.challenge, "abc123");
assert_eq!(challenge.token, "xyz");
assert_eq!(challenge.event_type, "url_verification");
}
}