<?php
/*
Plugin Name: Secure WP Nonces
Version: 1.0.0
Author: Matthew Sigley
Description: Reimplements the wp_create_nonce and wp_verify_nonce functions in a more secure way
*/

// Only override functions if none of them exist
if( !function_exists( 'wp_create_nonce' ) && !function_exists( 'wp_verify_nonce' ) && !function_exists( 'wp_create_nonce_hash' ) ) {
	function wp_create_nonce( $action = -1 ) {
		$user = wp_get_current_user();
		$uid  = (int) $user->ID;
		if ( ! $uid ) {
			/** This filter is documented in wp-includes/pluggable.php */
			$uid = apply_filters( 'nonce_user_logged_out', $uid, $action );
		}

		$token = '';
		if( function_exists( 'wp_get_session_token' ) )
			$token = wp_get_session_token();
		$i = wp_nonce_tick();

		$browser_id = $_SERVER['HTTP_USER_AGENT'];
		$browser_id = hash( 'crc32b', $_SERVER['HTTP_USER_AGENT'] );

		return wp_create_nonce_hash( $i, $action . '|' . $uid . '|' . $token );
	}

	// Amazon's S3 service uses a similar method for signing URLs
	// https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html
	function wp_create_nonce_hash( $nonce_tick, $action ) {
		$hash_algo = 'sha256';
		$actions = explode( '|', $action ); //This allows plugin authors to add signing steps by providing a nonce action delimited by pipes |.
		$num_actions = count( $actions );
		$salt = wp_salt( 'nonce' );
		$nonce = hash_hmac( $hash_algo, $nonce_tick, "SWPN{$salt}", true ); // The salt prefix allows for existing hashes to be invalidated if a change is made to the algorithm.

		for( $i = 1; $i < $num_actions; $i++ ) {
			$value = (string) $value;
			if( $value !== '' )
				$nonce = hash_hmac( $hash_algo, $actions[$i], $nonce, true );
		}

		$nonce = hash_hmac( 'sha256', $actions[0], $nonce );
		return $nonce;
	}

	function wp_verify_nonce( $nonce, $action = -1 ) {
		$nonce = (string) $nonce;
		$user  = wp_get_current_user();
		$uid   = (int) $user->ID;
		if ( ! $uid ) {
			/**
			 * Filters whether the user who generated the nonce is logged out.
			 *
			 * @since 3.5.0
			 *
			 * @param int    $uid    ID of the nonce-owning user.
			 * @param string $action The nonce action.
			 */
			$uid = apply_filters( 'nonce_user_logged_out', $uid, $action );
		}

		if ( empty( $nonce ) ) {
			return false;
		}

		$token = '';
		if( function_exists( 'wp_get_session_token' ) )
			$token = wp_get_session_token();
		$i = wp_nonce_tick();

		// Nonce generated 0-12 hours ago
		$expected = wp_create_nonce_hash( $i, $action . '|' . $uid . '|' . $token );
		if ( hash_equals( $expected, $nonce ) ) {
			return 1;
		}

		// Nonce generated 12-24 hours ago
		$expected = wp_create_nonce_hash( ( $i - 1 ), $action . '|' . $uid . '|' . $token );
		if ( hash_equals( $expected, $nonce ) ) {
			return 2;
		}

		/**
		 * Fires when nonce verification fails.
		 *
		 * @since 4.4.0
		 *
		 * @param string     $nonce  The invalid nonce.
		 * @param string|int $action The nonce action.
		 * @param WP_User    $user   The current user object.
		 * @param string     $token  The user's session token.
		 */
		do_action( 'wp_verify_nonce_failed', $nonce, $action, $user, $token );

		// Invalid nonce
		return false;
	}
}