<?php
/**
 * request_signing.php — Verifies the HMAC-SHA256 request signature sent
 * by the app's Loader.signForApp() on every app-facing API call.
 *
 * The app signs every request with:
 *   message = "{unix_timestamp_seconds}.{16-char-hex-nonce}.{hex(body)}"
 *   signature = HMAC-SHA256(key, message)
 *
 * Headers sent by the app:
 *   X-Timestamp: unix seconds
 *   X-Nonce: 16-char random hex
 *   X-Signature: lowercase hex HMAC
 *   X-Build-Id: static build identifier (informational, not verified)
 *
 * For GET requests (no body), the body hash portion is empty, so the
 * message is "{timestamp}.{nonce}." with a trailing dot.
 *
 * The shared secret is the same one baked into the app's native code
 * (Loader.smali → deobf → hex2bin). It is NOT customer-configurable and
 * must match exactly what the distributed app sends.
 *
 * NOTE: Only protect endpoints the app actually signs. The app's
 * FixturesApiClient and AppConfig (dns.php) do NOT sign their requests,
 * so those endpoints must NOT be protected here or the app breaks.
 */

/**
 * Verifies the request signature. Returns true if valid, false otherwise.
 * Call this at the top of any endpoint that should be protected.
 *
 * @param string $body Raw request body (empty string for GET requests)
 * @param int $maxAgeSeconds Maximum allowed clock skew / replay window
 */
function verify_request_signature(string $body = '', int $maxAgeSeconds = 300): bool {
    // The HMAC key, derived from the app's embedded obfuscated constant:
    //   Base64("b25kaGw5bj9rbWVqbD5paTo6PjlqPmhlbj44Omk+aGw") → XOR each byte with 0x5c
    //   → "32840e2c71960b55ffbe6b492bdf5b40" (hex) → 16 raw bytes via hex2bin
    $keyHex = '32840e2c71960b55ffbe6b492bdf5b40';
    $key = hex2bin($keyHex);

    $timestamp = $_SERVER['HTTP_X_TIMESTAMP'] ?? null;
    $nonce = $_SERVER['HTTP_X_NONCE'] ?? null;
    $signature = $_SERVER['HTTP_X_SIGNATURE'] ?? null;

    // All three headers must be present.
    if ($timestamp === null || $nonce === null || $signature === null) {
        return false;
    }

    // Timestamp must be a valid integer within the allowed window.
    if (!ctype_digit($timestamp)) {
        return false;
    }
    $ts = (int) $timestamp;
    $now = time();
    if (abs($now - $ts) > $maxAgeSeconds) {
        return false;
    }

    // Nonce must be a 16-char hex string (as generated by the app's randHex(16)).
    if (!preg_match('/^[0-9a-f]{16}$/', $nonce)) {
        return false;
    }

    // Reconstruct the exact message the app signed.
    // For GET requests (empty body), hex(body) = "" → message ends with a trailing dot.
    $bodyHex = bin2hex($body);
    $message = $timestamp . '.' . $nonce . '.' . $bodyHex;

    // Compute the expected HMAC and compare in constant time.
    $expected = hash_hmac('sha256', $message, $key);

    return hash_equals($expected, $signature);
}

/**
 * Convenience: verify and immediately reject with a 403 JSON response if
 * the signature is missing or invalid. Call at the top of protected
 * endpoints before any business logic.
 */
function require_valid_signature(string $body = '', int $maxAgeSeconds = 300): void {
    if (!verify_request_signature($body, $maxAgeSeconds)) {
        http_response_code(403);
        header('Content-Type: application/json; charset=UTF-8');
        echo json_encode(['success' => false, 'error' => 'Unauthorized']);
        exit;
    }
}
