//! HMAC-SHA256 helpers and `SigV4` signing-key derivation, implemented in-crate.
//!
//! Avoids a dependency on `aws-sigv4` or similar crates to keep the binary
//! small and the signing logic auditable in one place.

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

// ────────────────────────────────────────────────────────────────────────────
// SigV4 protocol constants
// ────────────────────────────────────────────────────────────────────────────

/// `SigV4` signing algorithm identifier, required verbatim in `Authorization`.
///
/// AWS rejects any other value; it must appear byte-for-byte in the
/// `Authorization` header and in the string-to-sign preamble.
pub const ALGORITHM: &str = "AWS4-HMAC-SHA256";

/// Credential-scope terminator with a dual role.
///
/// It closes the `date/region/service/aws4_request` credential scope string
/// **and** serves as the data for the final HMAC step in [`SigningKey::derive`].
pub const REQUEST_SUFFIX: &str = "aws4_request";

/// Magic prefix mandated by `SigV4` for the first HMAC key.
///
/// Prepended to the raw secret (`"AWS4" + secret`) before `kDate` is derived;
/// the literal `"AWS4"` is specified in the AWS documentation and is not
/// configurable.
const SECRET_PREFIX: &str = "AWS4";

/// Canonical header name: the HTTP `Host` field, always signed in `SigV4`.
///
/// Must be signed so the endpoint and bucket cannot be changed after the
/// request is signed; omitting it from `SignedHeaders` would allow
/// request hijacking.
pub const HDR_HOST: &str = "host";

/// Canonical header name for the assembled `SigV4` signature.
///
/// Appended last in [`to_http_header_pairs`] so the header carrying the
/// signature covers all headers listed in `SignedHeaders`.
///
/// [`to_http_header_pairs`]: crate::to_http_header_pairs
pub const HDR_AUTHORIZATION: &str = "authorization";

/// Canonical header name: hex-encoded SHA-256 of the request body.
pub const HDR_CONTENT_SHA256: &str = "x-amz-content-sha256";

/// Canonical header name: request timestamp (`YYYYMMDDTHHMMSSZ`).
pub const HDR_DATE: &str = "x-amz-date";

/// Canonical header name: AWS STS session token (optional).
pub const HDR_SECURITY_TOKEN: &str = "x-amz-security-token";

// ────────────────────────────────────────────────────────────────────────────
// SHA-256 / HMAC helpers
// ────────────────────────────────────────────────────────────────────────────

#[cfg_attr(
    feature = "tracing",
    tracing::instrument(level = "trace", skip(data), ret)
)]
/// SHA-256-digests `data` and returns the lowercase hex encoding.
///
/// `SigV4` requires lowercase hex throughout (body hash, canonical-request
/// hash, final signature); uppercase hex produces a signature AWS rejects.
pub fn sha256_hex(data: &[u8]) -> String {
    hex::encode(Sha256::digest(data))
}

#[cfg_attr(
    feature = "tracing",
    tracing::instrument(level = "trace", skip(key, data), ret)
)]
/// Computes `HMAC-SHA256(key, data)` and returns the raw bytes.
///
/// `SigV4` uses HMAC (not plain SHA) for key derivation so that each step
/// cryptographically depends on the previous one, preventing key reuse across
/// scopes. `new_from_slice` is infallible for HMAC-SHA256 because the spec
/// accepts any key length, so the `expect` below is always safe.
fn hmac_sha256(key: &[u8], data: &[u8]) -> Vec<u8> {
    #[expect(
        clippy::expect_used,
        reason = "HMAC accepts any key length; infallible"
    )]
    let mut mac = Hmac::<Sha256>::new_from_slice(key).expect("HMAC accepts any key length");
    mac.update(data);
    mac.finalize().into_bytes().to_vec()
}

#[cfg_attr(
    feature = "tracing",
    tracing::instrument(level = "trace", skip(key, data), ret)
)]
/// Produces the final `SigV4` signature: `HMAC-SHA256(key, data)` as lowercase hex.
///
/// This is the last step before the `Signature=…` value in `Authorization`;
/// lowercase hex is required for the same reason as in [`sha256_hex`].
fn hmac_sha256_hex(key: &[u8], data: &[u8]) -> String {
    hex::encode(hmac_sha256(key, data))
}

// ────────────────────────────────────────────────────────────────────────────
// SigV4 core
// ────────────────────────────────────────────────────────────────────────────

/// Derived `SigV4` signing key (opaque byte vector).
///
/// Kept opaque so the raw bytes are not accidentally serialised or logged;
/// leaking the derived key is equivalent to leaking the secret key for the
/// scope it was derived for.
#[derive(Debug)]
pub struct SigningKey(Vec<u8>);

impl SigningKey {
    #[cfg_attr(
        feature = "tracing",
        tracing::instrument(level = "trace", skip(secret, datestamp, region, service), ret)
    )]
    /// Derives the four-step `SigV4` signing key scoped to a single request.
    ///
    /// Steps: `kDate = HMAC("AWS4"+secret, date)` → `kRegion = HMAC(kDate, region)` →
    /// `kService = HMAC(kRegion, service)` → `kSigning = HMAC(kService, "aws4_request")`.
    /// Each step narrows the scope so the key is only valid for one date/region/service
    /// combination, limiting the blast radius of a key compromise.
    pub fn derive(secret: &str, datestamp: &str, region: &str, service: &str) -> Self {
        let k_date = hmac_sha256(
            format!("{SECRET_PREFIX}{secret}").as_bytes(),
            datestamp.as_bytes(),
        );
        let k_region = hmac_sha256(&k_date, region.as_bytes());
        let k_service = hmac_sha256(&k_region, service.as_bytes());
        let k_signing = hmac_sha256(&k_service, REQUEST_SUFFIX.as_bytes());
        Self(k_signing)
    }

    #[cfg_attr(
        feature = "tracing",
        tracing::instrument(level = "trace", skip(self, string_to_sign), ret)
    )]
    /// Produces the `Signature=…` value for the `Authorization` header.
    ///
    /// `string_to_sign` must follow the exact `SigV4` format
    /// (`ALGORITHM\ndate\nscope\nhash`); a single extra byte or wrong newline
    /// produces a silently different signature that AWS rejects without
    /// indicating where the mismatch is.
    pub fn sign_hex(&self, string_to_sign: &str) -> String {
        hmac_sha256_hex(&self.0, string_to_sign.as_bytes())
    }
}

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

    // ── SHA-256 ──────────────────────────────────────────────────────────────

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

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

    // ── SigningKey::derive – AWS General Reference vector ────────────────────

    /// Reference vector from the AWS `SigV4` documentation (IAM / us-east-1 /
    /// 2015-08-30). Source: "Examples of how to derive a signing key for
    /// Signature Version 4" in the AWS General Reference.
    #[test]
    fn signing_key_derive_iam_reference() {
        let key = SigningKey::derive(
            "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
            "20150830",
            "us-east-1",
            "iam",
        );
        assert_eq!(
            hex::encode(&key.0),
            "c4afb1cc5771d871763a393e44b703571b55cc28424d1a5e86da6ed3c154a4b9",
        );
    }

    /// End-to-end signature from the AWS `SigV4` "`ListUsers`" IAM example.
    #[test]
    fn sign_hex_iam_reference_list_users() {
        let key = SigningKey::derive(
            "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
            "20150830",
            "us-east-1",
            "iam",
        );
        let sts = "AWS4-HMAC-SHA256\n\
            20150830T123600Z\n\
            20150830/us-east-1/iam/aws4_request\n\
            f536975d06c0309214f805bb90ccff089219ecd68b2577efef23edd43b7e1a59";
        assert_eq!(
            key.sign_hex(sts),
            "5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7",
        );
    }

    // ── aws-sig-v4-test-suite vectors ────────────────────────────────────────
    //
    // Each test takes the .sts content verbatim (available in the test suite
    // ZIP at docs.aws.amazon.com) and asserts that sign_hex() produces the
    // Signature value found in the matching .authz file.
    //
    // Credentials used across all suite cases:
    //   secret  = wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY
    //   region  = us-east-1   service = service   date = 20150830

    fn suite_key() -> SigningKey {
        SigningKey::derive(
            "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
            "20150830",
            "us-east-1",
            "service",
        )
    }

    /// `get-vanilla` — GET / with empty body.
    #[test]
    fn suite_get_vanilla() {
        let sts = "AWS4-HMAC-SHA256\n\
            20150830T123600Z\n\
            20150830/us-east-1/service/aws4_request\n\
            bb579772317eb040ac9ed261061d46c1f17a8133879d6129b6e1c25292927e63";
        assert_eq!(
            suite_key().sign_hex(sts),
            "5fa00fa31553b73ebf1942676e86291e8372ff2a2260956d9b8aae1d763fbf31",
        );
    }

    /// `get-utf8` — GET /ሴ (path with `SigV4`-encoded UTF-8 segment).
    #[test]
    fn suite_get_utf8() {
        let sts = "AWS4-HMAC-SHA256\n\
            20150830T123600Z\n\
            20150830/us-east-1/service/aws4_request\n\
            2a0a97d02205e45ce2e994789806b19270cfbbb0921b278ccf58f5249ac42102";
        assert_eq!(
            suite_key().sign_hex(sts),
            "8318018e0b0f223aa2bbf98705b62bb787dc9c0e678f255a891fd03141be5d85",
        );
    }

    /// `get-unreserved` — GET with RFC 3986 unreserved chars in path.
    #[test]
    fn suite_get_unreserved() {
        let sts = "AWS4-HMAC-SHA256\n\
            20150830T123600Z\n\
            20150830/us-east-1/service/aws4_request\n\
            6a968768eefaa713e2a6b16b589a8ea192661f098f37349f4e2c0082757446f9";
        assert_eq!(
            suite_key().sign_hex(sts),
            "07ef7494c76fa4850883e2b006601f940f8a34d404d0cfa977f52a65bbf5f24f",
        );
    }

    /// `get-vanilla-query-order-key` — same key, two values sorted by value.
    #[test]
    fn suite_get_vanilla_query_order_key() {
        let sts = "AWS4-HMAC-SHA256\n\
            20150830T123600Z\n\
            20150830/us-east-1/service/aws4_request\n\
            704b4cef673542d84cdff252633f065e8daeba5f168b77116f8b1bcaf3d38f89";
        assert_eq!(
            suite_key().sign_hex(sts),
            "eedbc4e291e521cf13422ffca22be7d2eb8146eecf653089df300a15b2382bd1",
        );
    }

    /// `get-vanilla-query-order-key-case` — two distinct keys sorted.
    #[test]
    fn suite_get_vanilla_query_order_key_case() {
        let sts = "AWS4-HMAC-SHA256\n\
            20150830T123600Z\n\
            20150830/us-east-1/service/aws4_request\n\
            816cd5b414d056048ba4f7c5386d6e0533120fb1fcfa93762cf0fc39e2cf19e0";
        assert_eq!(
            suite_key().sign_hex(sts),
            "b97d918cfa904a5beff61c982a1b6f458b799221646efd99d3219ec94cdf2500",
        );
    }

    /// `get-vanilla-query-order-value` — same key, values sorted.
    #[test]
    fn suite_get_vanilla_query_order_value() {
        let sts = "AWS4-HMAC-SHA256\n\
            20150830T123600Z\n\
            20150830/us-east-1/service/aws4_request\n\
            c968629d70850097a2d8781c9bf7edcb988b04cac14cca9be4acc3595f884606";
        assert_eq!(
            suite_key().sign_hex(sts),
            "5772eed61e12b33fae39ee5e7012498b51d56abc0abb7c60486157bd471c4694",
        );
    }

    /// `get-vanilla-utf8-query` — UTF-8 character as query key.
    #[test]
    fn suite_get_vanilla_utf8_query() {
        let sts = "AWS4-HMAC-SHA256\n\
            20150830T123600Z\n\
            20150830/us-east-1/service/aws4_request\n\
            eb30c5bed55734080471a834cc727ae56beb50e5f39d1bff6d0d38cb192a7073";
        assert_eq!(
            suite_key().sign_hex(sts),
            "2cdec8eed098649ff3a119c94853b13c643bcf08f8b0a1d91e12c9027818dd04",
        );
    }

    /// `post-vanilla` — POST / with empty body.
    #[test]
    fn suite_post_vanilla() {
        let sts = "AWS4-HMAC-SHA256\n\
            20150830T123600Z\n\
            20150830/us-east-1/service/aws4_request\n\
            553f88c9e4d10fc9e109e2aeb65f030801b70c2f6468faca261d401ae622fc87";
        assert_eq!(
            suite_key().sign_hex(sts),
            "5da7c1a2acd57cee7505fc6676e4e544621c30862966e37dddb68e92efbe5d6b",
        );
    }
}
