<?php
/*
Plugin Name: Secure WP Nonces
Version: 1.1.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' ) ) {
	// Adds a browser id to the nonce hash to add some variance to the hash output.
	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();

		// Use UA string as part of the hash to help prevent token swaping
		$browser_id = trim( (string) $_SERVER['HTTP_USER_AGENT'] );
		$browser_id = hash( 'crc32b', $_SERVER['HTTP_USER_AGENT'] );

		return wp_create_nonce_hash( $i, $action . '|' . $uid . '|' . $token . '|' . $browser_id );
	}

	// Amazon's S3 service uses a similar method for signing URLs.
	// https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html
	// This allows plugin authors to add signing steps by providing a nonce action delimited by pipes |.
	// For example "form-post|form-id|form-action" would result in 2 additional signing steps in addition to the 4 on every nonce hash.
	function wp_create_nonce_hash( $nonce_tick, $action ) {
		// Use sha256 for actions less then 56 bytes long, otherwise use sha512.
		// In practice this means sha512 is used for nonces for logged in users.
		// sha512 is faster than sha256 on 64bit platforms for longer strings.
		// sha512 is available on php 5.1+.
		// Intel provides a hardware version of sha256: https://en.wikipedia.org/wiki/Intel_SHA_extensions
		// ARM provides a hardware version of sh256 and sha512: https://en.wikipedia.org/wiki/AArch64#AArch64_features https://en.wikipedia.org/wiki/AArch64#ARMv8.4-A
		$hash_algo = 'sha256';
		if( strlen( $action ) > 56 )
			$hash_algo = 'sha512';
		$actions = explode( '|', $action );
		$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++ ) {
			$actions[$i] = (string) $actions[$i];
			if( $actions[$i] !== '' )
				$nonce = hash_hmac( $hash_algo, $actions[$i], $nonce, true );
		}

		// Truncate hash to 64 characters. This is the length of a sha256 hash.
		$nonce = substr( hash_hmac( $hash_algo, $actions[0], $nonce ), 0, 64 );
		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 ( '' === $nonce || strlen( $nonce ) !== 64 ) {
			return false;
		}

		$browser_id = trim( (string) $_SERVER['HTTP_USER_AGENT'] );
		// Check for invalid UA strings
		// We are validating only the <product>/<product-version> part of the UA string
		// Format is documented here: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent
		// This prevents lazy bots from submitting requests.
		if( '' === $browser_id || !preg_match( '#^[^/]+/[\d\.]+#', $browser_id ) )
			return false;

		$browser_id = hash( 'crc32b', $_SERVER['HTTP_USER_AGENT'] );

		$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 . '|' . $browser_id );
		if ( hash_equals( $expected, $nonce ) ) {
			return 1;
		}

		// Nonce generated 12-24 hours ago
		$expected = wp_create_nonce_hash( ( $i - 1 ), $action . '|' . $uid . '|' . $token . '|' . $browser_id );
		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, $browser_id );

		// Invalid nonce
		return false;
	}
}