sz-orm-storage 1.2.2

SZ-ORM Storage Extension - S3 real impl (s3-sdk feature) + 6 in-memory mock providers (Aliyun/Huawei/Qiniu/Tencent/UpYun/Default)
Documentation
//! # 通用签名工具模块
//!
//! 集中提供各云厂商真实后端共用的签名原语,避免在 `s3_compat` / `qiniu` 等模块中重复实现。
//!
//! ## 提供的能力
//!
//! - [`compute_hmac_sha256`] / [`hmac_sha256_raw`] / [`hex_hmac_sha256`] — HMAC-SHA256(AWS Sig V4 / 华为 OBS / 腾讯 COS 使用)
//! - [`compute_hmac_sha1`] — HMAC-SHA1(七牛 Kodo 签名使用)
//! - [`hex_sha256`] — SHA256 十六进制摘要(payload 哈希)
//! - [`derive_signing_key`] — AWS Sig V4 签名密钥派生(kSecret -> kDate -> kRegion -> kService -> kSigning)
//! - [`utc_now_components`] — 当前 UTC 时间戳(date_stamp / amz_date),不依赖 chrono
//!
//! ## 启用方式
//!
//! 本模块在启用 `s3-compat`(带来 `hmac` + `sha2`)或 `qiniu-kodo`(带来 `hmac` + `sha2` + `sha1`)时编译。

use hmac::{Hmac, Mac};
use sha2::{Digest, Sha256};

/// HMAC-SHA256 类型别名
type HmacSha256 = Hmac<Sha256>;

/// 计算 HMAC-SHA256 并返回原始字节
///
/// HMAC 接受任意长度密钥,内部不会失败(除非底层加密实现故障,此处用 `expect` 表达"不可能失败"的不变量)。
pub fn compute_hmac_sha256(key: &[u8], data: &[u8]) -> Vec<u8> {
    hmac_sha256_raw(key, data)
}

/// 计算 HMAC-SHA256 并返回原始字节(底层实现)
pub fn hmac_sha256_raw(key: &[u8], data: &[u8]) -> Vec<u8> {
    let mut mac = HmacSha256::new_from_slice(key).expect("HMAC accepts any key length");
    mac.update(data);
    mac.finalize().into_bytes().to_vec()
}

/// 计算 HMAC-SHA256 并返回十六进制字符串
pub fn hex_hmac_sha256(key: &[u8], data: &[u8]) -> String {
    hex::encode(hmac_sha256_raw(key, data))
}

/// 计算 SHA256 哈希并返回十六进制字符串
///
/// 用于 AWS Sig V4 的 `x-amz-content-sha256` payload 哈希。
pub fn hex_sha256(data: &[u8]) -> String {
    let mut hasher = Sha256::new();
    hasher.update(data);
    hex::encode(hasher.finalize())
}

/// 计算 HMAC-SHA1 并返回原始字节(七牛签名使用)
///
/// 仅在 `qiniu-kodo` feature(提供 `sha1` 依赖)下可用。
#[cfg(feature = "qiniu-kodo")]
pub fn compute_hmac_sha1(key: &[u8], data: &[u8]) -> Vec<u8> {
    use sha1::Sha1;
    type HmacSha1 = Hmac<Sha1>;
    let mut mac = HmacSha1::new_from_slice(key).expect("HMAC accepts any key length");
    mac.update(data);
    mac.finalize().into_bytes().to_vec()
}

/// 派生 AWS Sig V4 签名密钥
///
/// 签名链路:`kSecret -> kDate -> kRegion -> kService -> kSigning`
/// 每一步均为 HMAC-SHA256,首步以 `"AWS4" + kSecret` 作为密钥。
///
/// 该实现与 AWS 官方文档示例对齐(见 `tests/signing_test.rs` 中的对照测试)。
pub fn derive_signing_key(secret_key: &str, date_stamp: &str, region: &str, service: &str) -> Vec<u8> {
    let k_secret = format!("AWS4{secret_key}");
    let k_date = hmac_sha256_raw(k_secret.as_bytes(), date_stamp.as_bytes());
    let k_region = hmac_sha256_raw(&k_date, region.as_bytes());
    let k_service = hmac_sha256_raw(&k_region, service.as_bytes());
    hmac_sha256_raw(&k_service, b"aws4_request")
}

/// 获取当前 UTC 时间,返回 `(date_stamp YYYYMMDD, amz_date YYYYMMDDTHHMMSSZ)`
///
/// 不依赖 chrono,使用 Howard Hinnant 的 `civil_from_days` 算法由 Unix 天数反推年月日。
pub fn utc_now_components() -> (String, String) {
    use std::time::{SystemTime, UNIX_EPOCH};
    let now = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default();
    let secs = now.as_secs();
    let days = (secs / 86400) as i64;
    let seconds_of_day = secs % 86400;
    let hour = seconds_of_day / 3600;
    let minute = (seconds_of_day % 3600) / 60;
    let second = seconds_of_day % 60;

    // Howard Hinnant civil_from_days:自 1970-01-01 起的天数 -> (y, m, d)
    let z = days + 719468;
    let era = if z >= 0 { z } else { z - 146096 } / 146097;
    let doe = z - era * 146097;
    let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
    let y = yoe + era * 400;
    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
    let mp = (5 * doy + 2) / 153;
    let d = doy - (153 * mp + 2) / 5 + 1;
    let m = if mp < 10 { mp + 3 } else { mp - 9 };
    let y_final = if m <= 2 { y + 1 } else { y };

    let date = format!("{y_final:04}{m:02}{d:02}");
    let amz = format!("{y_final:04}{m:02}{d:02}T{hour:02}{minute:02}{second:02}Z");
    (date, amz)
}

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

    #[test]
    fn test_hex_sha256_empty() {
        assert_eq!(
            hex_sha256(b""),
            "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
        );
    }

    #[test]
    fn test_hex_sha256_abc() {
        assert_eq!(
            hex_sha256(b"abc"),
            "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
        );
    }

    #[test]
    fn test_utc_now_components_format() {
        let (date, amz) = utc_now_components();
        assert_eq!(date.len(), 8);
        assert_eq!(amz.len(), 16);
        assert!(amz.starts_with(&date));
        assert!(amz.ends_with('Z'));
        assert_eq!(&amz[8..9], "T");
    }

    #[test]
    fn test_derive_signing_key_matches_aws_doc_example() {
        // AWS 官方文档示例:secret=wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY
        // date=20150830, region=us-east-1, service=iam
        // 期望签名 key(十六进制):
        // c4afb1cc5771d871763a393e44b703571b55cc28424d1a5e86da6ed3c154a4b9
        let key = derive_signing_key(
            "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
            "20150830",
            "us-east-1",
            "iam",
        );
        assert_eq!(
            hex::encode(key),
            "c4afb1cc5771d871763a393e44b703571b55cc28424d1a5e86da6ed3c154a4b9"
        );
    }

    #[test]
    fn test_compute_hmac_sha256_known_vector() {
        // RFC 4231 测试用例 1:key=0x0b*20, data="Hi There"
        let key = [0x0bu8; 20];
        let mac = compute_hmac_sha256(&key, b"Hi There");
        assert_eq!(
            hex::encode(&mac),
            "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7"
        );
    }

    #[cfg(feature = "qiniu-kodo")]
    #[test]
    fn test_compute_hmac_sha1_known_vector() {
        // RFC 2202 测试用例 1:key=0x0b*20, data="Hi There"
        let key = [0x0bu8; 20];
        let mac = compute_hmac_sha1(&key, b"Hi There");
        assert_eq!(
            hex::encode(&mac),
            "b617318655057264e28bc0b6fb378c8ef146be00"
        );
    }
}