#include "php_snuffleupagus.h"

// condensed version of PHP's php_hash_do_hash_hmac() in ext/hash/hash.c
#if PHP_VERSION_ID < 80000
static inline void *php_hash_alloc_context(const php_hash_ops *ops) {
  /* Zero out context memory so serialization doesn't expose internals */
  return ecalloc(1, ops->context_size);
}
#endif

static zend_string *sp_do_hash_hmac_sha256(char* restrict data, size_t data_len, char* restrict key, size_t key_len)
{
#if PHP_VERSION_ID < 80000
  const php_hash_ops *ops = php_hash_fetch_ops(ZEND_STRL("sha256"));
#else
  zend_string *algo = zend_string_init(ZEND_STRL("sha256"), 0);
  const php_hash_ops *ops = php_hash_fetch_ops(algo);
  zend_string_release_ex(algo, 0);
#endif

#if PHP_VERSION_ID < 70200
  if (!ops) {
#else
  if (!ops || !ops->is_crypto) {
#endif
    sp_log_err("hmac", "unsupported hash algorithm: sha256");
    return NULL;
  }

  void *context = php_hash_alloc_context(ops);

  unsigned char *K = emalloc(ops->block_size);
  zend_string *digest = zend_string_alloc(ops->digest_size, 0);

  php_hash_hmac_prep_key(K, ops, context, (unsigned char *) key, key_len);
  php_hash_hmac_round((unsigned char *) ZSTR_VAL(digest), ops, context, K, (unsigned char *) data, data_len);
  php_hash_string_xor_char(K, K, 0x6A, ops->block_size);
  php_hash_hmac_round((unsigned char *) ZSTR_VAL(digest), ops, context, K, (unsigned char *) ZSTR_VAL(digest), ops->digest_size);

  /* Zero the key */
  ZEND_SECURE_ZERO(K, ops->block_size);
  efree(K);
  efree(context);

  zend_string *hex_digest = zend_string_safe_alloc(ops->digest_size, 2, 0, 0);

  php_hash_bin2hex(ZSTR_VAL(hex_digest), (unsigned char *) ZSTR_VAL(digest), ops->digest_size);
  ZSTR_VAL(hex_digest)[2 * ops->digest_size] = 0;
  zend_string_release_ex(digest, 0);
  return hex_digest;
}

PHP_FUNCTION(sp_serialize) {
  zif_handler orig_handler;

  /* Call the original `serialize` function. */
  orig_handler = zend_hash_str_find_ptr(SPG(sp_internal_functions_hook), ZEND_STRL("serialize"));
  if (orig_handler) {
    orig_handler(INTERNAL_FUNCTION_PARAM_PASSTHRU);
  }

  if (!SPCFG(unserialize).enable) {
    return;
  }

  /* Compute the HMAC of the textual representation of the serialized data*/
  zend_string *hmac = sp_do_hash_hmac_sha256(Z_STRVAL_P(return_value), Z_STRLEN_P(return_value), ZSTR_VAL(SPCFG(encryption_key)), ZSTR_LEN(SPCFG(encryption_key)));

  if (!hmac) {
    zend_bailout();
  }

  size_t len = Z_STRLEN_P(return_value) + ZSTR_LEN(hmac);
  if (len < Z_STRLEN_P(return_value)) {
    // LCOV_EXCL_START
    sp_log_err("overflow_error",
               "Overflow tentative detected in sp_serialize.");
    zend_bailout();
    // LCOV_EXCL_STOP
  }

  /* Append the computed HMAC to the serialized data. */
  zend_string *orig_ret_str = return_value->value.str;
  RETVAL_NEW_STR(zend_string_concat2(Z_STRVAL_P(return_value), Z_STRLEN_P(return_value), ZSTR_VAL(hmac), ZSTR_LEN(hmac)));
  zend_string_free(orig_ret_str);
}

PHP_FUNCTION(sp_unserialize) {
  char *buf = NULL;
  size_t buf_len = 0;
  HashTable *opts = NULL;

  ZEND_PARSE_PARAMETERS_START(1, 2)
    Z_PARAM_STRING(buf, buf_len)
    Z_PARAM_OPTIONAL
    Z_PARAM_ARRAY_HT(opts)
  ZEND_PARSE_PARAMETERS_END();

  if (SPCFG(unserialize_noclass).enable) {
#if PHP_VERSION_ID > 80000
    HashTable ht;
    zend_hash_init(&ht, 1, NULL, NULL, 0);
    zval zv;
    ZVAL_FALSE(&zv);
    zend_hash_str_add(&ht, "allowed_classes", sizeof("allowed_classes")-1, &zv);
    php_unserialize_with_options(return_value, buf, buf_len, &ht, "unserialize");
    return;
#else
    sp_log_drop("unserialize_noclass", "unserialize_noclass is only supported on PHP8+");
#endif
  }

  const sp_config_unserialize *config_unserialize = &(SPCFG(unserialize));
#if ! (PHP_VERSION_ID >= 80300)
  zif_handler orig_handler = zend_hash_str_find_ptr(SPG(sp_internal_functions_hook), ZEND_STRL("unserialize"));
#endif

  /* 64 is the length of HMAC-256 */
  if (buf_len < 64) {
    if (true == config_unserialize->simulation) {
      sp_log_simulation("unserialize", "The serialized object is smaller than 64: assuming there is no HMAC");
#if PHP_VERSION_ID >= 80300
      // PHP8.3 gives a warning about trailing data in unserialize strings.
      php_unserialize_with_options(return_value, buf, buf_len, opts, "unserialize");
#else
      if ((orig_handler)) {
        orig_handler(INTERNAL_FUNCTION_PARAM_PASSTHRU);
      }
#endif
      return;
    } else {
      sp_log_drop("unserialize", "The serialized object is too small.");
    }
  }

  char* hmac = buf + buf_len - 64;
  char* serialized_str = ecalloc(buf_len - 64 + 1, 1);
  memcpy(serialized_str, buf, buf_len - 64);

  zend_string *expected_hmac = sp_do_hash_hmac_sha256(serialized_str, strlen(serialized_str), ZSTR_VAL(SPCFG(encryption_key)), ZSTR_LEN(SPCFG(encryption_key)));

  unsigned int status = 0;
  if (expected_hmac) {
    for (uint8_t i = 0; i < 64; i++) {
      status |= (hmac[i] ^ (ZSTR_VAL(expected_hmac))[i]);
    }
  } else { status = 1; }

  if (0 == status) {
#if PHP_VERSION_ID >= 80300
      // PHP8.3 gives a warning about trailing data in unserialize strings.
      php_unserialize_with_options(return_value, buf, buf_len - 64, opts, "unserialize");
#else
    if ((orig_handler)) {
      orig_handler(INTERNAL_FUNCTION_PARAM_PASSTHRU);
    }
#endif
  } else {
    if (config_unserialize->dump) {
      sp_log_request(config_unserialize->dump,
                     config_unserialize->textual_representation);
    }
    if (true == config_unserialize->simulation) {
      sp_log_simulation("unserialize", "Invalid HMAC for %s", serialized_str);
#if PHP_VERSION_ID >= 80300
      // PHP8.3 gives a warning about trailing data in unserialize strings.
      php_unserialize_with_options(return_value, buf, buf_len - 64, opts, "unserialize");
#else
      if ((orig_handler)) {
        orig_handler(INTERNAL_FUNCTION_PARAM_PASSTHRU);
      }
#endif
    } else {
      sp_log_drop("unserialize", "Invalid HMAC for %s", serialized_str);
    }
  }
  efree(serialized_str);
  return;
}

int hook_serialize(void) {
  TSRMLS_FETCH();

  HOOK_FUNCTION("serialize", sp_internal_functions_hook, PHP_FN(sp_serialize));
  HOOK_FUNCTION("unserialize", sp_internal_functions_hook,
                PHP_FN(sp_unserialize));

  return SUCCESS;
}
